Merge branch 'dev' into daikin-enhancements

This commit is contained in:
James Nimmo 2023-09-30 07:36:38 +13:00 committed by GitHub
commit c014793f92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
787 changed files with 40273 additions and 8018 deletions

View file

@ -5,9 +5,12 @@ Checks: >-
-altera-*, -altera-*,
-android-*, -android-*,
-boost-*, -boost-*,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-narrowing-conversions, -bugprone-narrowing-conversions,
-bugprone-signed-char-misuse, -bugprone-signed-char-misuse,
-cert-dcl50-cpp, -cert-dcl50-cpp,
-cert-err33-c,
-cert-err58-cpp, -cert-err58-cpp,
-cert-oop57-cpp, -cert-oop57-cpp,
-cert-str34-c, -cert-str34-c,
@ -15,6 +18,7 @@ Checks: >-
-clang-analyzer-osx.*, -clang-analyzer-osx.*,
-clang-diagnostic-delete-abstract-non-virtual-dtor, -clang-diagnostic-delete-abstract-non-virtual-dtor,
-clang-diagnostic-delete-non-abstract-non-virtual-dtor, -clang-diagnostic-delete-non-abstract-non-virtual-dtor,
-clang-diagnostic-ignored-optimization-argument,
-clang-diagnostic-shadow-field, -clang-diagnostic-shadow-field,
-clang-diagnostic-unused-const-variable, -clang-diagnostic-unused-const-variable,
-clang-diagnostic-unused-parameter, -clang-diagnostic-unused-parameter,
@ -25,6 +29,7 @@ Checks: >-
-cppcoreguidelines-macro-usage, -cppcoreguidelines-macro-usage,
-cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-prefer-member-initializer,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-pro-bounds-pointer-arithmetic,
@ -36,6 +41,7 @@ Checks: >-
-cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-special-member-functions, -cppcoreguidelines-special-member-functions,
-cppcoreguidelines-virtual-class-destructor,
-fuchsia-multiple-inheritance, -fuchsia-multiple-inheritance,
-fuchsia-overloaded-operator, -fuchsia-overloaded-operator,
-fuchsia-statically-constructed-objects, -fuchsia-statically-constructed-objects,
@ -68,6 +74,7 @@ Checks: >-
-modernize-use-nodiscard, -modernize-use-nodiscard,
-mpi-*, -mpi-*,
-objc-*, -objc-*,
-readability-container-data-pointer,
-readability-convert-member-functions-to-static, -readability-convert-member-functions-to-static,
-readability-else-after-return, -readability-else-after-return,
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
@ -82,8 +89,6 @@ WarningsAsErrors: '*'
AnalyzeTemporaryDtors: false AnalyzeTemporaryDtors: false
FormatStyle: google FormatStyle: google
CheckOptions: CheckOptions:
- key: google-readability-braces-around-statements.ShortStatementLines
value: '1'
- key: google-readability-function-size.StatementThreshold - key: google-readability-function-size.StatementThreshold
value: '800' value: '800'
- key: google-runtime-int.TypeSuffix - key: google-runtime-int.TypeSuffix
@ -158,3 +163,9 @@ CheckOptions:
value: '' value: ''
- key: readability-qualified-auto.AddConstToQualified - key: readability-qualified-auto.AddConstToQualified
value: 0 value: 0
- key: readability-identifier-length.MinimumVariableNameLength
value: 0
- key: readability-identifier-length.MinimumParameterNameLength
value: 0
- key: readability-identifier-length.MinimumLoopCounterNameLength
value: 0

View file

@ -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
View file

@ -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

View file

@ -0,0 +1,38 @@
name: Restore Python
inputs:
python-version:
description: Python version to restore
required: true
type: string
cache-key:
description: Cache key to use
required: true
type: string
outputs:
python-version:
description: Python version restored
value: ${{ steps.python.outputs.python-version }}
runs:
using: "composite"
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v4.7.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
with:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
shell: bash
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 .

View file

@ -8,7 +8,7 @@ on:
branches: [dev, beta, release] branches: [dev, beta, release]
paths: paths:
- "docker/**" - "docker/**"
- ".github/workflows/**" - ".github/workflows/ci-docker.yml"
- "requirements*.txt" - "requirements*.txt"
- "platformio.ini" - "platformio.ini"
- "script/platformio_install_deps.py" - "script/platformio_install_deps.py"
@ -16,7 +16,7 @@ on:
pull_request: pull_request:
paths: paths:
- "docker/**" - "docker/**"
- ".github/workflows/**" - ".github/workflows/ci-docker.yml"
- "requirements*.txt" - "requirements*.txt"
- "platformio.ini" - "platformio.ini"
- "script/platformio_install_deps.py" - "script/platformio_install_deps.py"
@ -40,15 +40,15 @@ jobs:
arch: [amd64, armv7, aarch64] arch: [amd64, armv7, aarch64]
build_type: ["ha-addon", "docker", "lint"] build_type: ["ha-addon", "docker", "lint"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4.1.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4.7.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3.0.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3.0.0
- name: Set TAG - name: Set TAG
run: | run: |

View file

@ -7,65 +7,268 @@ on:
branches: [dev, beta, release] branches: [dev, beta, release]
pull_request: pull_request:
paths:
- "**"
- "!.github/workflows/*.yml"
- ".github/workflows/ci.yml"
merge_group: merge_group:
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
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.2
with:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
- 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 .
black:
name: Check black
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- 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-list:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
- name: Find all YAML test files
id: set-matrix
run: echo "matrix=$(ls tests/test*.yaml | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
compile-tests:
name: Run YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- compile-tests-list
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 5 max-parallel: 2
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome compile ${{ matrix.file }}
run: |
. venv/bin/activate
esphome compile ${{ matrix.file }}
clang-tidy:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
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 +293,62 @@ 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@v4.1.0
uses: actions/setup-python@v4 - name: Restore Python
id: python uses: ./.github/actions/restore-python
with: with:
python-version: "3.9" python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache virtualenv
uses: actions/cache@v3
with:
path: .venv
# yamllint disable-line rule:line-length
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.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
- name: Cache platformio - name: Cache platformio
uses: actions/cache@v3 uses: actions/cache@v3.3.2
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-14
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
- 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

View file

@ -18,7 +18,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v4 - uses: dessant/lock-threads@v4.0.1
with: with:
pr-inactive-days: "1" pr-inactive-days: "1"
pr-lock-reason: "" pr-lock-reason: ""

View file

@ -19,7 +19,7 @@ jobs:
outputs: outputs:
tag: ${{ steps.tag.outputs.tag }} tag: ${{ steps.tag.outputs.tag }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4.1.0
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@ -43,15 +43,17 @@ jobs:
if: github.repository == 'esphome/esphome' && github.event_name == 'release' if: github.repository == 'esphome/esphome' && github.event_name == 'release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4.1.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4.7.0
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
@ -86,24 +88,24 @@ jobs:
target: "lint" target: "lint"
baseimg: "docker" baseimg: "docker"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4.1.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4.7.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3.0.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3.0.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@v2 uses: docker/login-action@v3.0.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@v2 uses: docker/login-action@v3.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -117,7 +119,7 @@ jobs:
--suffix "${{ matrix.image.suffix }}" --suffix "${{ matrix.image.suffix }}"
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5.0.0
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
@ -139,7 +141,7 @@ jobs:
needs: [deploy-docker] needs: [deploy-docker]
steps: steps:
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@v6 uses: actions/github-script@v6.4.1
with: with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: | script: |

View file

@ -18,7 +18,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v8.0.0
with: with:
days-before-pr-stale: 90 days-before-pr-stale: 90
days-before-pr-close: 7 days-before-pr-close: 7
@ -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
@ -38,7 +38,7 @@ jobs:
close-issues: close-issues:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v8.0.0
with: with:
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1

View file

@ -4,28 +4,25 @@ name: Synchronise Device Classes from Home Assistant
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '45 6 * * *' - cron: "45 6 * * *"
permissions:
contents: write
pull-requests: write
jobs: jobs:
sync: sync:
name: Sync Device Classes name: Sync Device Classes
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4.1.0
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@v3 uses: actions/checkout@v4.1.0
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4.7.0
with: with:
python-version: 3.11 python-version: 3.11
@ -38,23 +35,14 @@ jobs:
run: | run: |
python ./script/sync-device_class.py python ./script/sync-device_class.py
- name: Get PR template
id: pr-template-body
run: |
body=$(cat .github/PULL_REQUEST_TEMPLATE.md)
delimiter="$(openssl rand -hex 8)"
echo "body<<$delimiter" >> $GITHUB_OUTPUT
echo "$body" >> $GITHUB_OUTPUT
echo "$delimiter" >> $GITHUB_OUTPUT
- name: Commit changes - name: Commit changes
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v5.0.2
with: with:
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-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}

22
.github/workflows/yaml-lint.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: YAML lint
on:
push:
branches: [dev, beta, release]
paths:
- "**.yaml"
- "**.yml"
pull_request:
paths:
- "**.yaml"
- "**.yml"
jobs:
yamllint:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
- name: Run yamllint
uses: frenck/action-yamllint@v1.4.1

10
.gitignore vendored
View file

@ -13,6 +13,12 @@ __pycache__/
# Intellij Idea # Intellij Idea
.idea .idea
# Eclipse
.project
.cproject
.pydevproject
.settings/
# Vim # Vim
*.swp *.swp
@ -129,4 +135,6 @@ tests/.esphome/
sdkconfig.* sdkconfig.*
!sdkconfig.defaults !sdkconfig.defaults
.tests/ .tests/
/components

View file

@ -2,8 +2,8 @@
# See https://pre-commit.com for more information # See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.3.0 rev: 23.9.1
hooks: hooks:
- id: black - id: black
args: args:
@ -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.1 rev: v3.10.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]

18
.vscode/tasks.json vendored
View file

@ -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": []
} }
] ]
} }

View file

@ -11,14 +11,18 @@ esphome/*.py @esphome/core
esphome/core/* @esphome/core esphome/core/* @esphome/core
# Integrations # Integrations
esphome/components/a01nyub/* @MrSuicideParrot
esphome/components/absolute_humidity/* @DAVe3283 esphome/components/absolute_humidity/* @DAVe3283
esphome/components/ac_dimmer/* @glmnet esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core 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 @kpfleming @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/alpha3/* @jan-hofmeier
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
@ -29,6 +33,7 @@ esphome/components/api/* @OttoWinter
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter esphome/components/async_tcp/* @OttoWinter
esphome/components/atc_mithermometer/* @ahpohl esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner
esphome/components/b_parasite/* @rbaron esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan esphome/components/ballu/* @bazuchan
esphome/components/bang_bang/* @OttoWinter esphome/components/bang_bang/* @OttoWinter
@ -37,18 +42,21 @@ esphome/components/bedjet/climate/* @jhansche
esphome/components/bedjet/fan/* @jhansche esphome/components/bedjet/fan/* @jhansche
esphome/components/bh1750/* @OttoWinter esphome/components/bh1750/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0939/* @ziceva esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias- esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas esphome/components/bl0942/* @dbuezas
esphome/components/ble_client/* @buxtronix esphome/components/ble_client/* @buxtronix
esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bmi160/* @flaviut
esphome/components/bmp3xx/* @martgras esphome/components/bmp3xx/* @martgras
esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/button/* @esphome/core esphome/components/button/* @esphome/core
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @MrEditor97 esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
@ -74,16 +82,18 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz esphome/components/ektf2232/* @jesserockz
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble/* @jesserockz
esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_client/* @jesserockz
esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_ble_server/* @clydebarrow @jesserockz
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
@ -94,30 +104,37 @@ esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/fs3000/* @kahrendt esphome/components/fs3000/* @kahrendt
esphome/components/gcja5/* @gcormier
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle esphome/components/gps/* @coogle
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte esphome/components/growatt_solar/* @leeuwte
esphome/components/haier/* @Yarikx esphome/components/haier/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior 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/hrxl_maxsonar_wr/* @netmikey esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hte501/* @Stock-M esphome/components/hte501/* @Stock-M
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/ili9xxx/* @nielsnl68 esphome/components/i2s_audio/speaker/* @jesserockz
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core esphome/components/improv_base/* @esphome/core
esphome/components/improv_serial/* @esphome/core esphome/components/improv_serial/* @esphome/core
esphome/components/ina260/* @MrEditor97 esphome/components/ina260/* @mreditor97
esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkbird_ibsth1_mini/* @fkirill
esphome/components/inkplate6/* @jesserockz esphome/components/inkplate6/* @jesserockz
esphome/components/integration/* @OttoWinter esphome/components/integration/* @OttoWinter
@ -129,9 +146,12 @@ esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
@ -139,6 +159,7 @@ esphome/components/ltr390/* @sjtrny
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger
esphome/components/max6956/* @looping40
esphome/components/max7219digit/* @rspaargaren esphome/components/max7219digit/* @rspaargaren
esphome/components/max9611/* @mckaymatthew esphome/components/max9611/* @mckaymatthew
esphome/components/mcp23008/* @jesserockz esphome/components/mcp23008/* @jesserockz
@ -152,7 +173,7 @@ esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner esphome/components/mcp3204/* @rsumner
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
esphome/components/mcp9600/* @MrEditor97 esphome/components/mcp9600/* @mreditor97
esphome/components/mcp9808/* @k7hpn esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
@ -188,12 +209,15 @@ esphome/components/nfc/* @jesserockz
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @hwstar esphome/components/pca9554/* @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984 esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie esphome/components/pm1006/* @habbie
esphome/components/pmsa003i/* @sjtrny esphome/components/pmsa003i/* @sjtrny
esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz
esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz
@ -213,7 +237,9 @@ 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/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
@ -222,6 +248,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath esphome/components/sdp3x/* @Azimath
esphome/components/selec_meter/* @sourabhjaiswal esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras esphome/components/sen5x/* @martgras
esphome/components/sensirion_common/* @martgras esphome/components/sensirion_common/* @martgras
@ -234,7 +261,7 @@ esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sigma_delta_output/* @Cat-Ion esphome/components/sigma_delta_output/* @Cat-Ion
esphome/components/sim800l/* @glmnet esphome/components/sim800l/* @glmnet
esphome/components/sm10bit_base/* @Cossid esphome/components/sm10bit_base/* @Cossid
esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/sm2135/* @BoukeHaarsma23 @dd32 @matika77
esphome/components/sm2235/* @Cossid esphome/components/sm2235/* @Cossid
esphome/components/sm2335/* @Cossid esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus esphome/components/sml/* @alengwenus
@ -242,7 +269,10 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/spi/* @esphome/core esphome/components/speaker/* @jesserockz
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
esphome/components/sprinkler/* @kbx81 esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_base/* @kbx81
@ -267,18 +297,22 @@ 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
esphome/components/touchscreen/* @jesserockz esphome/components/touchscreen/* @jesserockz
esphome/components/tsl2591/* @wjcarpenter esphome/components/tsl2591/* @wjcarpenter
esphome/components/tt21100/* @kroimon
esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/binary_sensor/* @jesserockz
esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/climate/* @jesserockz
esphome/components/tuya/number/* @frankiboy1 esphome/components/tuya/number/* @frankiboy1
@ -295,13 +329,17 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz esphome/components/voice_assistant/* @jesserockz
esphome/components/wake_on_lan/* @willwill2will54 esphome/components/wake_on_lan/* @willwill2will54
esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra
esphome/components/whirlpool/* @glmnet esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
esphome/components/wl_134/* @hobbypunk90 esphome/components/wl_134/* @hobbypunk90
esphome/components/x9c/* @EtienneMD esphome/components/x9c/* @EtienneMD
esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68 esphome/components/xpt2046/* @nielsnl68 @numo68
esphome/components/zio_ultrasonic/* @kahrendt

View file

@ -22,14 +22,24 @@ RUN \
python3=3.9.2-3 \ python3=3.9.2-3 \
python3-pip=20.3.4-4+deb11u1 \ python3-pip=20.3.4-4+deb11u1 \
python3-setuptools=52.0.0-4 \ python3-setuptools=52.0.0-4 \
python3-pil=8.1.2+dfsg-0.3+deb11u1 \
python3-cryptography=3.3.2-1 \ python3-cryptography=3.3.2-1 \
python3-venv=3.9.2-3 \ python3-venv=3.9.2-3 \
iputils-ping=3:20210202-1 \ iputils-ping=3:20210202-1 \
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 \
&& rm -rf \ python3-cffi=1.14.5-1 \
libcairo2=1.16.0-5 \
patch=2.7.6-7; \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
apt-get install -y --no-install-recommends \
build-essential=12.9 \
python3-dev=3.9.2-3 \
zlib1g-dev=1:1.2.11.dfsg-2+deb11u2 \
libjpeg-dev=1:2.0.6-4 \
libfreetype-dev=2.10.4+dfsg-1+deb11u1; \
fi; \
rm -rf \
/tmp/* \ /tmp/* \
/var/{cache,log}/* \ /var/{cache,log}/* \
/var/lib/apt/lists/* /var/lib/apt/lists/*
@ -52,7 +62,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.11 \
# 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 \
@ -63,7 +73,7 @@ RUN \
COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini /
RUN \ RUN \
pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
&& /platformio_install_deps.py /platformio.ini && /platformio_install_deps.py /platformio.ini --libraries
# ======================= docker-type image ======================= # ======================= docker-type image =======================

View file

@ -35,11 +35,21 @@ if bashio::config.has_value 'default_compile_process_limit'; then
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit') export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
else else
if grep -q 'Raspberry Pi 3' /proc/cpuinfo; then if grep -q 'Raspberry Pi 3' /proc/cpuinfo; then
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1; export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1
fi fi
fi fi
mkdir -p "${pio_cache_base}" mkdir -p "${pio_cache_base}"
if bashio::fs.directory_exists '/config/esphome/.esphome'; then
bashio::log.info "Migrating old .esphome directory..."
if bashio::fs.file_exists '/config/esphome/.esphome/esphome.json'; then
mv /config/esphome/.esphome/esphome.json /data/esphome.json
fi
mkdir -p "/data/storage"
mv /config/esphome/.esphome/*.json /data/storage/ || true
rm -rf /config/esphome/.esphome
fi
bashio::log.info "Starting ESPHome dashboard..." bashio::log.info "Starting ESPHome dashboard..."
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon

View file

@ -18,18 +18,23 @@ 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,
CONF_PLATFORMIO_OPTIONS, CONF_PLATFORMIO_OPTIONS,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
SECRETS_FILES, SECRETS_FILES,
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import indent from esphome.helpers import indent, is_ip_address
from esphome.util import ( from esphome.util import (
run_external_command, run_external_command,
run_external_process, run_external_process,
@ -42,7 +47,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 +58,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,15 +79,19 @@ 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))
if default == "SERIAL":
return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
options.append((f"Over The Air ({CORE.address})", CORE.address)) 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 +99,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):
@ -209,14 +220,16 @@ def compile_program(args, config):
return 0 if idedata is not None else 1 return 0 if idedata is not None else 1
def upload_using_esptool(config, port): def upload_using_esptool(config, port, file):
from esphome import platformio_api from esphome import platformio_api
first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
"upload_speed", 460800 "upload_speed", 460800
) )
def run_esptool(baud_rate): if file is not None:
flash_images = [platformio_api.FlashImage(path=file, offset="0x0")]
else:
idedata = platformio_api.get_idedata(config) idedata = platformio_api.get_idedata(config)
firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" firmware_offset = "0x10000" if CORE.is_esp32 else "0x0"
@ -227,12 +240,13 @@ def upload_using_esptool(config, port):
*idedata.extra_flash_images, *idedata.extra_flash_images,
] ]
mcu = "esp8266" mcu = "esp8266"
if CORE.is_esp32: if CORE.is_esp32:
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
mcu = get_esp32_variant().lower() mcu = get_esp32_variant().lower()
def run_esptool(baud_rate):
cmd = [ cmd = [
"esptool.py", "esptool.py",
"--before", "--before",
@ -271,36 +285,55 @@ def upload_using_esptool(config, port):
return run_esptool(115200) return run_esptool(115200)
def upload_using_platformio(config, port):
from esphome import platformio_api
upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def upload_program(config, args, host): def upload_program(config, args, host):
if get_port_type(host) == "SERIAL": if get_port_type(host) == "SERIAL":
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
return upload_using_esptool(config, host) file = getattr(args, "file", None)
return upload_using_esptool(config, host, file)
if CORE.target_platform in (PLATFORM_RP2040): if CORE.target_platform in (PLATFORM_RP2040):
from esphome import platformio_api return upload_using_platformio(config, args.device)
upload_args = ["-t", "upload"] if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
if args.device is not None: return upload_using_platformio(config, host)
upload_args += ["--upload-port", args.device]
return platformio_api.run_platformio_cli_run(
config, CORE.verbose, *upload_args
)
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 (
not is_ip_address(CORE.address)
and (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 +343,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)
@ -338,10 +378,16 @@ def command_wizard(args):
def command_config(args, config): def command_config(args, config):
_LOGGER.info("Configuration is valid!")
if not CORE.verbose: if not CORE.verbose:
config = strip_default_ids(config) config = strip_default_ids(config)
safe_print(yaml_util.dump(config, args.show_secrets)) output = yaml_util.dump(config, args.show_secrets)
# add the console decoration so the front-end can hide the secrets
if not args.show_secrets:
output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
)
safe_print(output)
_LOGGER.info("Configuration is valid!")
return 0 return 0
@ -374,6 +420,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 +429,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 +445,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 +464,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 +478,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 +682,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 +771,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.",
@ -932,6 +1001,8 @@ def run_esphome(argv):
_LOGGER.error(e, exc_info=args.verbose) _LOGGER.error(e, exc_info=args.verbose)
return 1 return 1
_LOGGER.info("ESPHome %s", const.__version__)
for conf_path in args.configuration: for conf_path in args.configuration:
if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path) _LOGGER.warning("Skipping secrets file %s", conf_path)

View file

@ -0,0 +1 @@
CODEOWNERS = ["@MrSuicideParrot"]

View file

@ -0,0 +1,57 @@
// Datasheet https://wiki.dfrobot.com/A01NYUB%20Waterproof%20Ultrasonic%20Sensor%20SKU:%20SEN0313
#include "a01nyub.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace a01nyub {
static const char *const TAG = "a01nyub.sensor";
static const uint8_t MAX_DATA_LENGTH_BYTES = 4;
void A01nyubComponent::loop() {
uint8_t data;
while (this->available() > 0) {
if (this->read_byte(&data)) {
buffer_.push_back(data);
this->check_buffer_();
}
}
}
void A01nyubComponent::check_buffer_() {
if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) {
size_t i;
for (i = 0; i < this->buffer_.size(); i++) {
// Look for the first packet
if (this->buffer_[i] == 0xFF) {
if (i + 1 + 3 < this->buffer_.size()) { // Packet is not complete
return; // Wait for completion
}
uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF;
if (this->buffer_[i + 3] == checksum) {
float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2];
if (distance > 280) {
float meters = distance / 1000.0;
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
}
break;
}
}
this->buffer_.clear();
}
}
void A01nyubComponent::dump_config() {
ESP_LOGCONFIG(TAG, "A01nyub Sensor:");
LOG_SENSOR(" ", "Distance", this);
}
} // namespace a01nyub
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace a01nyub {
class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
// Nothing really public.
// ========== INTERNAL METHODS ==========
void loop() override;
void dump_config() override;
protected:
void check_buffer_();
std::vector<uint8_t> buffer_;
};
} // namespace a01nyub
} // namespace esphome

View file

@ -0,0 +1,41 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_METER,
ICON_ARROW_EXPAND_VERTICAL,
DEVICE_CLASS_DISTANCE,
)
CODEOWNERS = ["@MrSuicideParrot"]
DEPENDENCIES = ["uart"]
a01nyub_ns = cg.esphome_ns.namespace("a01nyub")
A01nyubComponent = a01nyub_ns.class_(
"A01nyubComponent", sensor.Sensor, cg.Component, uart.UARTDevice
)
CONFIG_SCHEMA = sensor.sensor_schema(
A01nyubComponent,
unit_of_measurement=UNIT_METER,
icon=ICON_ARROW_EXPAND_VERTICAL,
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_DISTANCE,
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"a01nyub",
baud_rate=9600,
require_tx=False,
require_rx=True,
data_bits=8,
parity=None,
stop_bits=1,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View file

@ -28,6 +28,6 @@ async def to_code(config):
dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN]) dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN])
cg.add(var.set_dir_pin(dir_pin)) cg.add(var.set_dir_pin(dir_pin))
if CONF_SLEEP_PIN in config: if sleep_pin_config := config.get(CONF_SLEEP_PIN):
sleep_pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN]) sleep_pin = await cg.gpio_pin_expression(sleep_pin_config)
cg.add(var.set_sleep_pin(sleep_pin)) cg.add(var.set_sleep_pin(sleep_pin))

View file

@ -1 +1,193 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ANALOG, CONF_INPUT
from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant
from esphome.const import PLATFORM_ESP8266
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
ATTENUATION_MODES = {
"0db": cg.global_ns.ADC_ATTEN_DB_0,
"2.5db": cg.global_ns.ADC_ATTEN_DB_2_5,
"6db": cg.global_ns.ADC_ATTEN_DB_6,
"11db": cg.global_ns.ADC_ATTEN_DB_11,
"auto": "auto",
}
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
# pin to adc1 channel mapping
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7,
},
VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
},
VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
},
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
},
VARIANT_ESP32H2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
}
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
# TODO: add other variants
VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1,
2: adc2_channel_t.ADC2_CHANNEL_2,
15: adc2_channel_t.ADC2_CHANNEL_3,
13: adc2_channel_t.ADC2_CHANNEL_4,
12: adc2_channel_t.ADC2_CHANNEL_5,
14: adc2_channel_t.ADC2_CHANNEL_6,
27: adc2_channel_t.ADC2_CHANNEL_7,
25: adc2_channel_t.ADC2_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9,
},
VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9,
},
VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9,
},
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
}
def validate_adc_pin(value):
if str(value).upper() == "VCC":
if CORE.is_rp2040:
return pins.internal_gpio_input_pin_schema(29)
return cv.only_on([PLATFORM_ESP8266])("VCC")
if str(value).upper() == "TEMPERATURE":
return cv.only_on_rp2040("TEMPERATURE")
if CORE.is_esp32:
value = pins.internal_gpio_input_pin_number(value)
variant = get_esp32_variant()
if (
variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
and variant not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
):
raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported")
if (
value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
and value not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
return pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266:
value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
value
)
if value != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
if CORE.is_rp2040:
value = pins.internal_gpio_input_pin_number(value)
if value not in (26, 27, 28, 29):
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
return pins.internal_gpio_input_pin_schema(value)
if CORE.is_libretiny:
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
raise NotImplementedError

View file

@ -1,6 +1,6 @@
#include "adc_sensor.h" #include "adc_sensor.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#ifdef USE_ADC_SENSOR_VCC #ifdef USE_ADC_SENSOR_VCC
@ -12,6 +12,9 @@ ADC_MODE(ADC_VCC)
#endif #endif
#ifdef USE_RP2040 #ifdef USE_RP2040
#ifdef CYW43_USES_VSYS_PIN
#include "pico/cyw43_arch.h"
#endif
#include <hardware/adc.h> #include <hardware/adc.h>
#endif #endif
@ -20,15 +23,15 @@ namespace adc {
static const char *const TAG = "adc"; static const char *const TAG = "adc";
// 13bit for S2, and 12bit for all other esp32 variants // 13-bit for S2, 12-bit for all other ESP32 variants
#ifdef USE_ESP32 #ifdef USE_ESP32
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
#ifndef SOC_ADC_RTC_MAX_BITWIDTH #ifndef SOC_ADC_RTC_MAX_BITWIDTH
#if USE_ESP32_VARIANT_ESP32S2 #if USE_ESP32_VARIANT_ESP32S2
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
#else #else
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
#endif #endif
#endif #endif
@ -47,14 +50,21 @@ extern "C"
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); if (channel1_ != ADC1_CHANNEL_MAX) {
if (!autorange_) { adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
adc1_config_channel_atten(channel_, attenuation_); if (!autorange_) {
adc1_config_channel_atten(channel1_, attenuation_);
}
} else if (channel2_ != ADC2_CHANNEL_MAX) {
if (!autorange_) {
adc2_config_channel_atten(channel2_, attenuation_);
}
} }
// load characteristics for each attenuation // load characteristics for each attenuation
for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { for (int32_t i = 0; i <= ADC_ATTEN_DB_11; i++) {
auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
1100, // default vref 1100, // default vref
&cal_characteristics_[i]); &cal_characteristics_[i]);
switch (cal_value) { switch (cal_value) {
@ -85,13 +95,13 @@ extern "C"
void ADCSensor::dump_config() { void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this); LOG_SENSOR("", "ADC Sensor", this);
#ifdef USE_ESP8266 #if defined(USE_ESP8266) || defined(USE_LIBRETINY)
#ifdef USE_ADC_SENSOR_VCC #ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC"); ESP_LOGCONFIG(TAG, " Pin: VCC");
#else #else
LOG_PIN(" Pin: ", pin_); LOG_PIN(" Pin: ", pin_);
#endif #endif
#endif // USE_ESP8266 #endif // USE_ESP8266 || USE_LIBRETINY
#ifdef USE_ESP32 #ifdef USE_ESP32
LOG_PIN(" Pin: ", pin_); LOG_PIN(" Pin: ", pin_);
@ -116,13 +126,19 @@ void ADCSensor::dump_config() {
} }
} }
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_RP2040 #ifdef USE_RP2040
if (this->is_temperature_) { if (this->is_temperature_) {
ESP_LOGCONFIG(TAG, " Pin: Temperature"); ESP_LOGCONFIG(TAG, " Pin: Temperature");
} else { } else {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", pin_); LOG_PIN(" Pin: ", pin_);
#endif // USE_ADC_SENSOR_VCC
} }
#endif #endif // USE_RP2040
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
@ -136,9 +152,9 @@ void ADCSensor::update() {
#ifdef USE_ESP8266 #ifdef USE_ESP8266
float ADCSensor::sample() { float ADCSensor::sample() {
#ifdef USE_ADC_SENSOR_VCC #ifdef USE_ADC_SENSOR_VCC
int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) int32_t raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else #else
int raw = analogRead(this->pin_->get_pin()); // NOLINT int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT
#endif #endif
if (output_raw_) { if (output_raw_) {
return raw; return raw;
@ -150,29 +166,53 @@ float ADCSensor::sample() {
#ifdef USE_ESP32 #ifdef USE_ESP32
float ADCSensor::sample() { float ADCSensor::sample() {
if (!autorange_) { if (!autorange_) {
int raw = adc1_get_raw(channel_); int raw = -1;
if (channel1_ != ADC1_CHANNEL_MAX) {
raw = adc1_get_raw(channel1_);
} else if (channel2_ != ADC2_CHANNEL_MAX) {
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
}
if (raw == -1) { if (raw == -1) {
return NAN; return NAN;
} }
if (output_raw_) { if (output_raw_) {
return raw; return raw;
} }
uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]);
return mv / 1000.0f; return mv / 1000.0f;
} }
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; int raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
raw11 = adc1_get_raw(channel_); if (channel1_ != ADC1_CHANNEL_MAX) {
if (raw11 < ADC_MAX) { adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11);
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); raw11 = adc1_get_raw(channel1_);
raw6 = adc1_get_raw(channel_); if (raw11 < ADC_MAX) {
if (raw6 < ADC_MAX) { adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6);
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); raw6 = adc1_get_raw(channel1_);
raw2 = adc1_get_raw(channel_); if (raw6 < ADC_MAX) {
if (raw2 < ADC_MAX) { adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5);
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); raw2 = adc1_get_raw(channel1_);
raw0 = adc1_get_raw(channel_); if (raw2 < ADC_MAX) {
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(channel1_);
}
}
}
} else if (channel2_ != ADC2_CHANNEL_MAX) {
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_11);
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw11);
if (raw11 < ADC_MAX) {
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_6);
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6);
if (raw6 < ADC_MAX) {
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_2_5);
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2);
if (raw2 < ADC_MAX) {
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_0);
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0);
}
} }
} }
} }
@ -181,10 +221,10 @@ float ADCSensor::sample() {
return NAN; return NAN;
} }
uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_11]);
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
uint32_t c11 = std::min(raw11, ADC_HALF); uint32_t c11 = std::min(raw11, ADC_HALF);
@ -206,23 +246,54 @@ float ADCSensor::sample() {
adc_set_temp_sensor_enabled(true); adc_set_temp_sensor_enabled(true);
delay(1); delay(1);
adc_select_input(4); adc_select_input(4);
int32_t raw = adc_read();
adc_set_temp_sensor_enabled(false);
if (this->output_raw_) {
return raw;
}
return raw * 3.3f / 4096.0f;
} else { } else {
uint8_t pin = this->pin_->get_pin(); uint8_t pin = this->pin_->get_pin();
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
// VSYS ADC both share GPIO29
cyw43_thread_enter();
}
#endif // CYW43_USES_VSYS_PIN
adc_gpio_init(pin); adc_gpio_init(pin);
adc_select_input(pin - 26); adc_select_input(pin - 26);
}
int raw = adc_read(); int32_t raw = adc_read();
if (this->is_temperature_) {
adc_set_temp_sensor_enabled(false); #ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
cyw43_thread_exit();
}
#endif // CYW43_USES_VSYS_PIN
if (output_raw_) {
return raw;
}
float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0;
return raw * 3.3f / 4096.0f * coeff;
} }
if (output_raw_) {
return raw;
}
return raw * 3.3f / 4096.0f;
} }
#endif #endif
#ifdef USE_LIBRETINY
float ADCSensor::sample() {
if (output_raw_) {
return analogRead(this->pin_->get_pin()); // NOLINT
}
return analogReadVoltage(this->pin_->get_pin()) / 1000.0f; // NOLINT
}
#endif // USE_LIBRETINY
#ifdef USE_ESP8266 #ifdef USE_ESP8266
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
#endif #endif

View file

@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#ifdef USE_ESP32 #ifdef USE_ESP32
/// Set the attenuation for this pin. Only available on the ESP32. /// Set the attenuation for this pin. Only available on the ESP32.
void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; }
void set_channel(adc1_channel_t channel) { channel_ = channel; } void set_channel1(adc1_channel_t channel) {
channel1_ = channel;
channel2_ = ADC2_CHANNEL_MAX;
}
void set_channel2(adc2_channel_t channel) {
channel2_ = channel;
channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { autorange_ = autorange; } void set_autorange(bool autorange) { autorange_ = autorange; }
#endif #endif
/// Update adc values. /// Update ADC values
void update() override; void update() override;
/// Setup ADc /// Setup ADC
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
/// `HARDWARE_LATE` setup priority. /// `HARDWARE_LATE` setup priority
float get_setup_priority() const override; float get_setup_priority() const override;
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
void set_output_raw(bool output_raw) { output_raw_ = output_raw; } void set_output_raw(bool output_raw) { output_raw_ = output_raw; }
@ -52,9 +59,14 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#ifdef USE_ESP32 #ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0}; adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc1_channel_t channel_{}; adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false}; bool autorange_{false};
esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; #if ESP_IDF_VERSION_MAJOR >= 5
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#else
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
#endif
#endif #endif
}; };

View file

@ -1,137 +1,49 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins import esphome.final_validate as fv
from esphome.core import CORE
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant
from esphome.const import ( from esphome.const import (
CONF_ATTENUATION, CONF_ATTENUATION,
CONF_RAW,
CONF_ID, CONF_ID,
CONF_INPUT,
CONF_NUMBER, CONF_NUMBER,
CONF_PIN, CONF_PIN,
CONF_RAW,
CONF_WIFI,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE from . import (
from esphome.components.esp32 import get_esp32_variant ATTENUATION_MODES,
from esphome.components.esp32.const import ( ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
VARIANT_ESP32, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
VARIANT_ESP32C3, validate_adc_pin,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
) )
AUTO_LOAD = ["voltage_sampler"] AUTO_LOAD = ["voltage_sampler"]
ATTENUATION_MODES = {
"0db": cg.global_ns.ADC_ATTEN_DB_0,
"2.5db": cg.global_ns.ADC_ATTEN_DB_2_5,
"6db": cg.global_ns.ADC_ATTEN_DB_6,
"11db": cg.global_ns.ADC_ATTEN_DB_11,
"auto": "auto",
}
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
# pin to adc1 channel mapping
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7,
},
VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
},
VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
},
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32H2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
}
def validate_adc_pin(value):
if str(value).upper() == "VCC":
return cv.only_on_esp8266("VCC")
if str(value).upper() == "TEMPERATURE":
return cv.only_on_rp2040("TEMPERATURE")
if CORE.is_esp32:
value = pins.internal_gpio_input_pin_number(value)
variant = get_esp32_variant()
if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL:
raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported")
if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]:
raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
return pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266:
from esphome.components.esp8266.gpio import CONF_ANALOG
value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
value
)
if value != 17: # A0
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.")
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
if CORE.is_rp2040:
value = pins.internal_gpio_input_pin_number(value)
if value not in (26, 27, 28, 29):
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.")
return pins.internal_gpio_input_pin_schema(value)
raise NotImplementedError
def validate_config(config): def validate_config(config):
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
return config
def final_validate_config(config):
if CORE.is_esp32:
variant = get_esp32_variant()
if (
CONF_WIFI in fv.full_config.get()
and config[CONF_PIN][CONF_NUMBER]
in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
raise cv.Invalid(
f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
)
return config return config
@ -161,6 +73,8 @@ CONFIG_SCHEMA = cv.All(
validate_config, validate_config,
) )
FINAL_VALIDATE_SCHEMA = final_validate_config
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@ -175,17 +89,26 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin)) cg.add(var.set_pin(pin))
if CONF_RAW in config: cg.add(var.set_output_raw(config[CONF_RAW]))
cg.add(var.set_output_raw(config[CONF_RAW]))
if CONF_ATTENUATION in config: if attenuation := config.get(CONF_ATTENUATION):
if config[CONF_ATTENUATION] == "auto": if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true)) cg.add(var.set_autorange(cg.global_ns.true))
else: else:
cg.add(var.set_attenuation(config[CONF_ATTENUATION])) cg.add(var.set_attenuation(attenuation))
if CORE.is_esp32: if CORE.is_esp32:
variant = get_esp32_variant() variant = get_esp32_variant()
pin_num = config[CONF_PIN][CONF_NUMBER] pin_num = config[CONF_PIN][CONF_NUMBER]
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] if (
cg.add(var.set_channel(chan)) variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
):
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel1(chan))
elif (
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel2(chan))

View file

@ -48,16 +48,16 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await display.register_display(var, config) await display.register_display(var, config)
if CONF_PIXEL_MAPPER in config: if pixel_mapper := config.get(CONF_PIXEL_MAPPER):
pixel_mapper_template_ = await cg.process_lambda( pixel_mapper_template_ = await cg.process_lambda(
config[CONF_PIXEL_MAPPER], pixel_mapper,
[(int, "x"), (int, "y")], [(int, "x"), (int, "y")],
return_type=cg.int_, return_type=cg.int_,
) )
cg.add(var.set_pixel_mapper(pixel_mapper_template_)) cg.add(var.set_pixel_mapper(pixel_mapper_template_))
if CONF_LAMBDA in config: if lambda_config := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
) )
cg.add(var.set_writer(lambda_)) cg.add(var.set_writer(lambda_))

View file

@ -72,8 +72,8 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_IRQ_PIN in config: if irq_pin_config := config.get(CONF_IRQ_PIN):
irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) irq_pin = await cg.gpio_pin_expression(irq_pin_config)
cg.add(var.set_irq_pin(irq_pin)) cg.add(var.set_irq_pin(irq_pin))
for key in [ for key in [

View file

@ -45,10 +45,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config: if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config[CONF_HUMIDITY]) sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_humidity_sensor(sens))

View file

@ -1,5 +1,6 @@
#include "airthings_listener.h" #include "airthings_listener.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes>
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -19,7 +20,7 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
sn |= ((uint32_t) it.data[2] << 16); sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24); sn |= ((uint32_t) it.data[3] << 24);
ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str()); ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str());
return true; return true;
} }
} }

View file

@ -0,0 +1,103 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
CONF_BATTERY_VOLTAGE,
CONF_HUMIDITY,
CONF_PRESSURE,
CONF_TEMPERATURE,
CONF_TVOC,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PARTS_PER_BILLION,
UNIT_PERCENT,
UNIT_VOLT,
)
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
DEPENDENCIES = ["ble_client"]
CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval"
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,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
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_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(
CONF_BATTERY_UPDATE_INTERVAL,
default="24h",
): cv.update_interval,
}
)
.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 config_humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config_humidity)
cg.add(var.set_humidity(sens))
if config_temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config_temperature)
cg.add(var.set_temperature(sens))
if config_pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(config_pressure)
cg.add(var.set_pressure(sens))
if config_tvoc := config.get(CONF_TVOC):
sens = await sensor.new_sensor(config_tvoc)
cg.add(var.set_tvoc(sens))
if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(config_battery_voltage)
cg.add(var.set_battery_voltage(sens))
if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
cg.add(var.set_battery_update_interval(config_battery_update_interval))

View file

@ -0,0 +1,211 @@
#include "airthings_wave_base.h"
// All information related to reading battery information came from the sensors.airthings_wave
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
#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: {
this->handle_ = 0;
this->acp_handle_ = 0;
this->cccd_handle_ = 0;
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->request_read_values_()) {
if (!this->read_battery_next_update_) {
this->node_state = espbt::ClientState::ESTABLISHED;
} else {
// delay setting node_state to ESTABLISHED until confirmation of the notify registration
this->request_battery_();
}
}
// ensure that the client will be disconnected even if no responses arrive
this->set_response_timeout_();
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;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED;
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.conn_id != this->parent()->get_conn_id())
break;
if (param->notify.handle == this->acp_handle_) {
this->read_battery_(param->notify.value, param->notify.value_len);
}
break;
}
default:
break;
}
}
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; }
void AirthingsWaveBase::update() {
if (this->node_state != espbt::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");
}
}
}
bool AirthingsWaveBase::request_read_values_() {
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());
return false;
}
this->handle_ = chr->handle;
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);
return false;
}
this->response_pending_();
return true;
}
bool AirthingsWaveBase::request_battery_() {
uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND;
uint8_t cccd_value[2] = {1, 0};
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
this->service_uuid_.to_string().c_str(),
this->access_control_point_characteristic_uuid_.to_string().c_str());
return false;
}
auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
if (descr == nullptr) {
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
this->access_control_point_characteristic_uuid_.to_string().c_str());
return false;
}
auto reg_status =
esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle);
if (reg_status) {
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status);
return false;
}
this->acp_handle_ = chr->handle;
this->cccd_handle_ = descr->handle;
auto descr_status =
esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_,
2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (descr_status) {
ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status);
return false;
}
auto chr_status =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1,
&battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (chr_status) {
ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status);
return false;
}
this->response_pending_();
return true;
}
void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
auto *value = (AccessControlPointResponse *) (&raw_value[2]);
if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) {
ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery);
if (this->battery_voltage_ != nullptr) {
float voltage = value->battery / 1000.0f;
this->battery_voltage_->publish_state(voltage);
}
// read the battery again at the configured update interval
if (this->battery_update_interval_ != this->update_interval_) {
this->read_battery_next_update_ = false;
this->set_timeout("battery", this->battery_update_interval_,
[this]() { this->read_battery_next_update_ = true; });
}
}
this->response_received_();
}
void AirthingsWaveBase::response_pending_() {
this->responses_pending_++;
this->set_response_timeout_();
}
void AirthingsWaveBase::response_received_() {
if (--this->responses_pending_ == 0) {
// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
this->parent()->set_enabled(false);
}
}
void AirthingsWaveBase::set_response_timeout_() {
this->set_timeout("response_timeout", 30 * 1000, [this]() {
this->responses_pending_ = 1;
this->response_received_();
});
}
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View file

@ -0,0 +1,90 @@
#pragma once
// All information related to reading battery levels came from the sensors.airthings_wave
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
#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 {
namespace espbt = esphome::esp32_ble_tracker;
static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d;
static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902);
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; }
void set_battery_voltage(sensor::Sensor *voltage) {
battery_voltage_ = voltage;
this->read_battery_next_update_ = true;
}
void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; }
protected:
bool is_valid_voc_value_(uint16_t voc);
bool request_read_values_();
virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
sensor::Sensor *battery_voltage_{nullptr};
uint16_t handle_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID sensors_data_characteristic_uuid_;
uint16_t acp_handle_{0};
uint16_t cccd_handle_{0};
espbt::ESPBTUUID access_control_point_characteristic_uuid_;
uint8_t responses_pending_{0};
void response_pending_();
void response_received_();
void set_response_timeout_();
// default to *not* reading battery voltage from the device; the
// set_* function for the battery sensor will set this to 'true'
bool read_battery_next_update_{false};
bool request_battery_();
void read_battery_(uint8_t *raw_value, uint16_t value_len);
uint32_t battery_update_interval_{};
struct AccessControlPointResponse {
uint32_t unused1;
uint8_t unused2;
uint8_t illuminance;
uint8_t unused3[10];
uint16_t unused4[4];
uint16_t battery;
uint16_t unused5;
};
};
} // namespace airthings_wave_base
} // namespace esphome
#endif // USE_ESP32

View file

@ -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) {
this->humidity_sensor_->publish_state(value->humidity / 100.0f); if (this->humidity_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f); this->humidity_sensor_->publish_state(value->humidity / 100.0f);
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); }
if (is_valid_voc_value_(value->voc)) {
if (this->pressure_sensor_ != nullptr) {
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);
}
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
// so other clients can connect to it (e.g. the
// mobile app).
parent()->set_enabled(false);
} }
}
bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } this->response_received_();
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_);
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
} }
AirthingsWaveMini::AirthingsWaveMini() AirthingsWaveMini::AirthingsWaveMini() {
: PollingComponent(10000), this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} this->access_control_point_characteristic_uuid_ =
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
}
} // namespace airthings_wave_mini } // namespace airthings_wave_mini
} // namespace esphome } // namespace esphome

View file

@ -2,50 +2,25 @@
#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 {
namespace espbt = esphome::esp32_ble_tracker;
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";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-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 *raw_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;

View file

@ -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))

View file

@ -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,72 +16,66 @@ 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);
this->humidity_sensor_->publish_state(value->humidity / 2.0f); if (this->humidity_sensor_ != nullptr) {
if (is_valid_radon_value_(value->radon)) { this->humidity_sensor_->publish_state(value->humidity / 2.0f);
this->radon_sensor_->publish_state(value->radon);
}
if (is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt);
}
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
if (is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
if (is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
} }
// This instance must not stay connected if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) {
// so other clients can connect to it (e.g. the this->radon_sensor_->publish_state(value->radon);
// mobile app). }
parent()->set_enabled(false);
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);
}
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
}
if (this->pressure_sensor_ != nullptr) {
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
}
if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
} 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);
} }
} }
this->response_received_();
} }
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 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 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(" ", "Battery Voltage", this->battery_voltage_);
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_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} this->access_control_point_characteristic_uuid_ =
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
}
} // namespace airthings_wave_plus } // namespace airthings_wave_plus
} // namespace esphome } // namespace esphome

View file

@ -2,58 +2,36 @@
#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 {
namespace espbt = esphome::esp32_ble_tracker;
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";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-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 *raw_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;

View file

@ -1,116 +1,64 @@
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_RADON): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
unit_of_measurement=UNIT_PERCENT, icon=ICON_RADIOACTIVE,
device_class=DEVICE_CLASS_HUMIDITY, accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0, ),
), cv.Optional(CONF_RADON_LONG_TERM): 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, accuracy_decimals=0,
accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT,
state_class=STATE_CLASS_MEASUREMENT, ),
), cv.Optional(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION,
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, accuracy_decimals=0,
icon=ICON_RADIOACTIVE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
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(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
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 config_radon := config.get(CONF_RADON):
sens = await sensor.new_sensor(config_radon)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_RADON in config:
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 config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) sens = await sensor.new_sensor(config_radon_long_term)
cg.add(var.set_radon_long_term(sens)) cg.add(var.set_radon_long_term(sens))
if CONF_TEMPERATURE in config: if config_co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(config_co2)
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:
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))

View file

@ -0,0 +1,250 @@
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"
CONF_ON_ARMING = "on_arming"
CONF_ON_PENDING = "on_pending"
CONF_ON_ARMED_HOME = "on_armed_home"
CONF_ON_ARMED_NIGHT = "on_armed_night"
CONF_ON_ARMED_AWAY = "on_armed_away"
CONF_ON_DISARMED = "on_disarmed"
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()
)
ArmingTrigger = alarm_control_panel_ns.class_(
"ArmingTrigger", automation.Trigger.template()
)
PendingTrigger = alarm_control_panel_ns.class_(
"PendingTrigger", automation.Trigger.template()
)
ArmedHomeTrigger = alarm_control_panel_ns.class_(
"ArmedHomeTrigger", automation.Trigger.template()
)
ArmedNightTrigger = alarm_control_panel_ns.class_(
"ArmedNightTrigger", automation.Trigger.template()
)
ArmedAwayTrigger = alarm_control_panel_ns.class_(
"ArmedAwayTrigger", automation.Trigger.template()
)
DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template()
)
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
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_ARMING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
}
),
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
}
),
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
}
),
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
}
),
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
}
),
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
}
),
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_ARMING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PENDING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_HOME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_NIGHT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_AWAY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISARMED, []):
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 code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, 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 code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, args, cg.std_string)
cg.add(var.set_code(templatable_))
return var
@automation.register_action(
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_arm_night_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 code_config := config.get(CONF_CODE):
templatable_ = await cg.templatable(code_config, 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")

View file

@ -0,0 +1,148 @@
#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();
} else if (state == ACP_STATE_ARMING) {
this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_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_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_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

View file

@ -0,0 +1,184 @@
#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 chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_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_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
};
} // namespace alarm_control_panel
} // namespace esphome

View file

@ -0,0 +1,104 @@
#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;
}
if (state == ACP_STATE_ARMED_NIGHT && (this->parent_->get_supported_features() & ACP_FEAT_ARM_NIGHT) == 0) {
ESP_LOGW(TAG, "Cannot arm night 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

View file

@ -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

View file

@ -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("ARMED_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

View file

@ -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

View file

@ -0,0 +1,177 @@
#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 ArmingTrigger : public Trigger<> {
public:
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
}
};
class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_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 ArmNightAction : public Action<Ts...> {
public:
explicit ArmNightAction(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_night();
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

View file

@ -0,0 +1 @@
CODEOWNERS = ["@jan-hofmeier"]

View file

@ -0,0 +1,189 @@
#include "alpha3.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <lwip/sockets.h> //gives ntohl
#ifdef USE_ESP32
namespace esphome {
namespace alpha3 {
static const char *const TAG = "alpha3";
void Alpha3::dump_config() {
ESP_LOGCONFIG(TAG, "ALPHA3");
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
LOG_SENSOR(" ", "Head", this->head_sensor_);
LOG_SENSOR(" ", "Power", this->power_sensor_);
LOG_SENSOR(" ", "Current", this->current_sensor_);
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
}
void Alpha3::setup() {}
void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
int16_t value_offset, sensor::Sensor *sensor, float factor) {
if (sensor == nullptr)
return;
// we need to handle cases where a value is split over two packets
const int16_t value_length = 4; // 32bit float
// offset inside current response packet
auto rel_offset = value_offset - response_offset;
if (rel_offset <= -value_length)
return; // aready passed the value completly
if (rel_offset >= length)
return; // value not in this packet
auto start_offset = std::max(0, rel_offset);
auto end_offset = std::min((int16_t) (rel_offset + value_length), length);
auto copy_length = end_offset - start_offset;
auto buffer_offset = std::max(-rel_offset, 0);
std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length);
if (rel_offset + value_length <= length) {
// we have the whole value
void *buffer = this->buffer_; // to prevent warnings when casting the pointer
*((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian
float fvalue = *((float *) buffer);
sensor->publish_state(fvalue * factor);
}
}
bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH);
}
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
if (this->response_offset_ >= this->response_length_) {
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
if (length < GENI_RESPONSE_HEADER_LENGTH) {
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
return;
}
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
response[0], response[1], response[2], response[3], response[4]);
return;
}
this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum
this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH;
std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH);
}
auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor,
float factor) {
this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor);
};
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F);
} else {
ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1],
this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5],
this->response_type_[6], this->response_type_[7]);
}
this->response_offset_ += length;
}
void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
this->response_offset_ = 0;
this->response_length_ = 0;
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
break;
}
case ESP_GATTC_CONNECT_EVT: {
if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0)
return;
auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT);
if (ret) {
ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret);
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
this->node_state = espbt::ClientState::IDLE;
if (this->flow_sensor_ != nullptr)
this->flow_sensor_->publish_state(NAN);
if (this->head_sensor_ != nullptr)
this->head_sensor_->publish_state(NAN);
if (this->power_sensor_ != nullptr)
this->power_sensor_->publish_state(NAN);
if (this->current_sensor_ != nullptr)
this->current_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
this->speed_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
this->voltage_sensor_->publish_state(NAN);
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
break;
}
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
chr->handle);
if (status) {
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
}
this->geni_handle_ = chr->handle;
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED;
this->update();
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle == this->geni_handle_) {
this->handle_geni_response_(param->notify.value, param->notify.value_len);
}
break;
}
default:
break;
}
}
void Alpha3::send_request_(uint8_t *request, size_t len) {
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
}
void Alpha3::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return;
}
if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) {
uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31};
this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head));
delay(25); // need to wait between requests
}
if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr ||
this->voltage_sensor_ != nullptr) {
uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205};
this->send_request_(geni_request_power, sizeof(geni_request_power));
delay(25); // need to wait between requests
}
}
} // namespace alpha3
} // namespace esphome
#endif

View file

@ -0,0 +1,73 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace alpha3 {
namespace espbt = esphome::esp32_ble_tracker;
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;
static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24};
static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0;
static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4;
static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37};
static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0;
static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4;
static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8;
static const int16_t GENI_RESPONSE_POWER_OFFSET = 12;
static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
void setup() 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 dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; }
void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; }
void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; }
protected:
sensor::Sensor *flow_sensor_{nullptr};
sensor::Sensor *head_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *speed_sensor_{nullptr};
sensor::Sensor *voltage_sensor_{nullptr};
uint16_t geni_handle_;
int16_t response_length_;
int16_t response_offset_;
uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH];
uint8_t buffer_[4];
void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
int16_t value_offset, sensor::Sensor *sensor, float factor);
void handle_geni_response_(const uint8_t *response, uint16_t length);
void send_request_(uint8_t *request, size_t len);
bool is_current_response_type_(const uint8_t *response_type);
};
} // namespace alpha3
} // namespace esphome
#endif

View file

@ -0,0 +1,85 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
CONF_ID,
CONF_CURRENT,
CONF_FLOW,
CONF_HEAD,
CONF_POWER,
CONF_SPEED,
CONF_VOLTAGE,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
UNIT_METER,
UNIT_CUBIC_METER_PER_HOUR,
UNIT_REVOLUTIONS_PER_MINUTE,
)
alpha3_ns = cg.esphome_ns.namespace("alpha3")
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Alpha3),
cv.Optional(CONF_FLOW): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
accuracy_decimals=2,
),
cv.Optional(CONF_HEAD): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=2,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
accuracy_decimals=2,
),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
),
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(cv.polling_component_schema("15s"))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if flow_config := config.get(CONF_FLOW):
sens = await sensor.new_sensor(flow_config)
cg.add(var.set_flow_sensor(sens))
if head_config := config.get(CONF_HEAD):
sens = await sensor.new_sensor(head_config)
cg.add(var.set_head_sensor(sens))
if power_config := config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(sens))
if current_config := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(sens))
if speed_config := config.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(var.set_speed_sensor(sens))
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))

View file

@ -47,10 +47,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config: if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config[CONF_HUMIDITY]) sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_humidity_sensor(sens))

View file

@ -44,10 +44,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
if CONF_BATTERY_LEVEL in config: if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery(sens)) cg.add(var.set_battery(sens))
if CONF_ILLUMINANCE in config: if illuminance_config := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) sens = await sensor.new_sensor(illuminance_config)
cg.add(var.set_illuminance(sens)) cg.add(var.set_illuminance(sens))

View file

@ -1,35 +1,124 @@
import logging import logging
from esphome import core from esphome import automation, core
from esphome.components import display, font from esphome.components import 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__)
AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
Animation_ = display.display_ns.class_("Animation", espImage.Image_) CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
animation_ns = cg.esphome_ns.namespace("animation")
Animation_ = animation_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
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_FILE): cv.file_, cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_RESIZE): cv.dimensions, cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( cv.Optional(CONF_RESIZE): cv.dimensions,
espImage.IMAGE_TYPE, upper=True cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
), 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)
CODEOWNERS = ["@syndlex"] NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
async def animation_action_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 (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config): async def to_code(config):
@ -50,16 +139,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 +159,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 +189,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 +205,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 +256,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
pos = x + y * width8 + (height * width8 * frameIndex) elif frame.getpixel((x, y)):
data[pos // 8] |= 0x80 >> (pos % 8) continue
pos = x + y * width8 + (height * width8 * frameIndex)
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 +288,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 loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frames)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))

View file

@ -0,0 +1,70 @@
#include "animation.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::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 animation
} // namespace esphome

View file

@ -0,0 +1,67 @@
#pragma once
#include "esphome/components/image/image.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace animation {
class Animation : public image::Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::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_;
};
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->next_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->prev_frame(); }
protected:
Animation *parent_;
};
template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame)
void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected:
Animation *parent_;
};
} // namespace animation
} // namespace esphome

View file

@ -4,7 +4,6 @@ from esphome.components import i2c
from esphome.const import CONF_ID from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "binary_sensor"]
MULTI_CONF = True MULTI_CONF = True
CONF_APDS9960_ID = "apds9960_id" CONF_APDS9960_ID = "apds9960_id"

View file

@ -116,8 +116,12 @@ void APDS9960::setup() {
APDS9960_WRITE_BYTE(0x80, val); APDS9960_WRITE_BYTE(0x80, val);
} }
bool APDS9960::is_color_enabled_() const { bool APDS9960::is_color_enabled_() const {
return this->red_channel_ != nullptr || this->green_channel_ != nullptr || this->blue_channel_ != nullptr || #ifdef USE_SENSOR
this->clear_channel_ != nullptr; return this->red_sensor_ != nullptr || this->green_sensor_ != nullptr || this->blue_sensor_ != nullptr ||
this->clear_sensor_ != nullptr;
#else
return false;
#endif
} }
void APDS9960::dump_config() { void APDS9960::dump_config() {
@ -125,6 +129,15 @@ void APDS9960::dump_config() {
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Red channel", this->red_sensor_);
LOG_SENSOR(" ", "Green channel", this->green_sensor_);
LOG_SENSOR(" ", "Blue channel", this->blue_sensor_);
LOG_SENSOR(" ", "Clear channel", this->clear_sensor_);
LOG_SENSOR(" ", "Proximity", this->proximity_sensor_);
#endif
if (this->is_failed()) { if (this->is_failed()) {
switch (this->error_code_) { switch (this->error_code_) {
case COMMUNICATION_FAILED: case COMMUNICATION_FAILED:
@ -181,17 +194,22 @@ void APDS9960::read_color_data_(uint8_t status) {
float blue_perc = (uint_blue / float(UINT16_MAX)) * 100.0f; float blue_perc = (uint_blue / float(UINT16_MAX)) * 100.0f;
ESP_LOGD(TAG, "Got clear=%.1f%% red=%.1f%% green=%.1f%% blue=%.1f%%", clear_perc, red_perc, green_perc, blue_perc); ESP_LOGD(TAG, "Got clear=%.1f%% red=%.1f%% green=%.1f%% blue=%.1f%%", clear_perc, red_perc, green_perc, blue_perc);
if (this->clear_channel_ != nullptr) #ifdef USE_SENSOR
this->clear_channel_->publish_state(clear_perc); if (this->clear_sensor_ != nullptr)
if (this->red_channel_ != nullptr) this->clear_sensor_->publish_state(clear_perc);
this->red_channel_->publish_state(red_perc); if (this->red_sensor_ != nullptr)
if (this->green_channel_ != nullptr) this->red_sensor_->publish_state(red_perc);
this->green_channel_->publish_state(green_perc); if (this->green_sensor_ != nullptr)
if (this->blue_channel_ != nullptr) this->green_sensor_->publish_state(green_perc);
this->blue_channel_->publish_state(blue_perc); if (this->blue_sensor_ != nullptr)
this->blue_sensor_->publish_state(blue_perc);
#endif
} }
void APDS9960::read_proximity_data_(uint8_t status) { void APDS9960::read_proximity_data_(uint8_t status) {
if (this->proximity_ == nullptr) #ifndef USE_SENSOR
return;
#else
if (this->proximity_sensor_ == nullptr)
return; return;
if ((status & 0b10) == 0x00) { if ((status & 0b10) == 0x00) {
@ -204,7 +222,8 @@ void APDS9960::read_proximity_data_(uint8_t status) {
float prox_perc = (prox / float(UINT8_MAX)) * 100.0f; float prox_perc = (prox / float(UINT8_MAX)) * 100.0f;
ESP_LOGD(TAG, "Got proximity=%.1f%%", prox_perc); ESP_LOGD(TAG, "Got proximity=%.1f%%", prox_perc);
this->proximity_->publish_state(prox_perc); this->proximity_sensor_->publish_state(prox_perc);
#endif
} }
void APDS9960::read_gesture_data_() { void APDS9960::read_gesture_data_() {
if (!this->is_gesture_enabled_()) if (!this->is_gesture_enabled_())
@ -256,28 +275,29 @@ void APDS9960::read_gesture_data_() {
} }
} }
void APDS9960::report_gesture_(int gesture) { void APDS9960::report_gesture_(int gesture) {
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *bin; binary_sensor::BinarySensor *bin;
switch (gesture) { switch (gesture) {
case 1: case 1:
bin = this->up_direction_; bin = this->up_direction_binary_sensor_;
this->gesture_up_started_ = false; this->gesture_up_started_ = false;
this->gesture_down_started_ = false; this->gesture_down_started_ = false;
ESP_LOGD(TAG, "Got gesture UP"); ESP_LOGD(TAG, "Got gesture UP");
break; break;
case 2: case 2:
bin = this->down_direction_; bin = this->down_direction_binary_sensor_;
this->gesture_up_started_ = false; this->gesture_up_started_ = false;
this->gesture_down_started_ = false; this->gesture_down_started_ = false;
ESP_LOGD(TAG, "Got gesture DOWN"); ESP_LOGD(TAG, "Got gesture DOWN");
break; break;
case 3: case 3:
bin = this->left_direction_; bin = this->left_direction_binary_sensor_;
this->gesture_left_started_ = false; this->gesture_left_started_ = false;
this->gesture_right_started_ = false; this->gesture_right_started_ = false;
ESP_LOGD(TAG, "Got gesture LEFT"); ESP_LOGD(TAG, "Got gesture LEFT");
break; break;
case 4: case 4:
bin = this->right_direction_; bin = this->right_direction_binary_sensor_;
this->gesture_left_started_ = false; this->gesture_left_started_ = false;
this->gesture_right_started_ = false; this->gesture_right_started_ = false;
ESP_LOGD(TAG, "Got gesture RIGHT"); ESP_LOGD(TAG, "Got gesture RIGHT");
@ -290,6 +310,7 @@ void APDS9960::report_gesture_(int gesture) {
bin->publish_state(true); bin->publish_state(true);
bin->publish_state(false); bin->publish_state(false);
} }
#endif
} }
void APDS9960::process_dataset_(int up, int down, int left, int right) { void APDS9960::process_dataset_(int up, int down, int left, int right) {
/* Algorithm: (see Figure 11 in datasheet) /* Algorithm: (see Figure 11 in datasheet)
@ -365,10 +386,22 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) {
} }
} }
float APDS9960::get_setup_priority() const { return setup_priority::DATA; } float APDS9960::get_setup_priority() const { return setup_priority::DATA; }
bool APDS9960::is_proximity_enabled_() const { return this->proximity_ != nullptr || this->is_gesture_enabled_(); } bool APDS9960::is_proximity_enabled_() const {
return
#ifdef USE_SENSOR
this->proximity_sensor_ != nullptr
#else
false
#endif
|| this->is_gesture_enabled_();
}
bool APDS9960::is_gesture_enabled_() const { bool APDS9960::is_gesture_enabled_() const {
return this->up_direction_ != nullptr || this->left_direction_ != nullptr || this->down_direction_ != nullptr || #ifdef USE_BINARY_SENSOR
this->right_direction_ != nullptr; return this->up_direction_binary_sensor_ != nullptr || this->left_direction_binary_sensor_ != nullptr ||
this->down_direction_binary_sensor_ != nullptr || this->right_direction_binary_sensor_ != nullptr;
#else
return false;
#endif
} }
} // namespace apds9960 } // namespace apds9960

View file

@ -1,14 +1,34 @@
#pragma once #pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#endif
namespace esphome { namespace esphome {
namespace apds9960 { namespace apds9960 {
class APDS9960 : public PollingComponent, public i2c::I2CDevice { class APDS9960 : public PollingComponent, public i2c::I2CDevice {
#ifdef USE_SENSOR
SUB_SENSOR(red)
SUB_SENSOR(green)
SUB_SENSOR(blue)
SUB_SENSOR(clear)
SUB_SENSOR(proximity)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(up_direction)
SUB_BINARY_SENSOR(right_direction)
SUB_BINARY_SENSOR(down_direction)
SUB_BINARY_SENSOR(left_direction)
#endif
public: public:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@ -23,16 +43,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice {
void set_gesture_gain(uint8_t gain) { this->gesture_gain_ = gain; } void set_gesture_gain(uint8_t gain) { this->gesture_gain_ = gain; }
void set_gesture_wait_time(uint8_t wait_time) { this->gesture_wait_time_ = wait_time; } void set_gesture_wait_time(uint8_t wait_time) { this->gesture_wait_time_ = wait_time; }
void set_red_channel(sensor::Sensor *red_channel) { red_channel_ = red_channel; }
void set_green_channel(sensor::Sensor *green_channel) { green_channel_ = green_channel; }
void set_blue_channel(sensor::Sensor *blue_channel) { blue_channel_ = blue_channel; }
void set_clear_channel(sensor::Sensor *clear_channel) { clear_channel_ = clear_channel; }
void set_up_direction(binary_sensor::BinarySensor *up_direction) { up_direction_ = up_direction; }
void set_right_direction(binary_sensor::BinarySensor *right_direction) { right_direction_ = right_direction; }
void set_down_direction(binary_sensor::BinarySensor *down_direction) { down_direction_ = down_direction; }
void set_left_direction(binary_sensor::BinarySensor *left_direction) { left_direction_ = left_direction; }
void set_proximity(sensor::Sensor *proximity) { proximity_ = proximity; }
protected: protected:
bool is_color_enabled_() const; bool is_color_enabled_() const;
bool is_proximity_enabled_() const; bool is_proximity_enabled_() const;
@ -50,15 +60,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice {
uint8_t gesture_gain_; uint8_t gesture_gain_;
uint8_t gesture_wait_time_; uint8_t gesture_wait_time_;
sensor::Sensor *red_channel_{nullptr};
sensor::Sensor *green_channel_{nullptr};
sensor::Sensor *blue_channel_{nullptr};
sensor::Sensor *clear_channel_{nullptr};
binary_sensor::BinarySensor *up_direction_{nullptr};
binary_sensor::BinarySensor *right_direction_{nullptr};
binary_sensor::BinarySensor *down_direction_{nullptr};
binary_sensor::BinarySensor *left_direction_{nullptr};
sensor::Sensor *proximity_{nullptr};
enum ErrorCode { enum ErrorCode {
NONE = 0, NONE = 0,
COMMUNICATION_FAILED, COMMUNICATION_FAILED,

View file

@ -6,19 +6,14 @@ from . import APDS9960, CONF_APDS9960_ID
DEPENDENCIES = ["apds9960"] DEPENDENCIES = ["apds9960"]
DIRECTIONS = { DIRECTIONS = ["up", "down", "left", "right"]
"UP": "set_up_direction",
"DOWN": "set_down_direction",
"LEFT": "set_left_direction",
"RIGHT": "set_right_direction",
}
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_MOVING device_class=DEVICE_CLASS_MOVING
).extend( ).extend(
{ {
cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960),
cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, lower=True),
} }
) )
@ -26,5 +21,5 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
async def to_code(config): async def to_code(config):
hub = await cg.get_variable(config[CONF_APDS9960_ID]) hub = await cg.get_variable(config[CONF_APDS9960_ID])
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]]) func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor")
cg.add(func(var)) cg.add(func(var))

View file

@ -11,13 +11,7 @@ from . import APDS9960, CONF_APDS9960_ID
DEPENDENCIES = ["apds9960"] DEPENDENCIES = ["apds9960"]
TYPES = { TYPES = ["clear", "red", "green", "blue", "proximity"]
"CLEAR": "set_clear_channel",
"RED": "set_red_channel",
"GREEN": "set_green_channel",
"BLUE": "set_blue_channel",
"PROXIMITY": "set_proximity",
}
CONFIG_SCHEMA = sensor.sensor_schema( CONFIG_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
@ -26,7 +20,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
).extend( ).extend(
{ {
cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), cv.Required(CONF_TYPE): cv.one_of(*TYPES, lower=True),
cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960),
} }
) )
@ -35,5 +29,5 @@ CONFIG_SCHEMA = sensor.sensor_schema(
async def to_code(config): async def to_code(config):
hub = await cg.get_variable(config[CONF_APDS9960_ID]) hub = await cg.get_variable(config[CONF_APDS9960_ID])
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
func = getattr(hub, TYPES[config[CONF_TYPE]]) func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor")
cg.add(func(var)) cg.add(func(var))

View file

@ -116,9 +116,8 @@ async def to_code(config):
cg.add(var.register_user_service(trigger)) cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf) await automation.build_automation(trigger, func_args, conf)
if CONF_ENCRYPTION in config: if encryption_config := config.get(CONF_ENCRYPTION):
conf = config[CONF_ENCRYPTION] decoded = base64.b64decode(encryption_config[CONF_KEY])
decoded = base64.b64decode(conf[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded))) cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.4") cg.add_library("esphome/noise-c", "0.1.4")

View file

@ -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,8 @@ message VoiceAssistantRequest {
option (ifdef) = "USE_VOICE_ASSISTANT"; option (ifdef) = "USE_VOICE_ASSISTANT";
bool start = 1; bool start = 1;
string conversation_id = 2;
bool use_vad = 3;
} }
message VoiceAssistantResponse { message VoiceAssistantResponse {
@ -1433,3 +1457,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;
}

View file

@ -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;
@ -223,6 +231,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) {
msg.assumed_state = traits.get_is_assumed_state(); msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_tilt = traits.get_supports_tilt(); msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop();
msg.device_class = cover->get_device_class(); msg.device_class = cover->get_device_class();
msg.disabled_by_default = cover->is_disabled_by_default(); msg.disabled_by_default = cover->is_disabled_by_default();
msg.icon = cover->get_icon(); msg.icon = cover->get_icon();
@ -844,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) {
@ -894,11 +907,13 @@ 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, bool use_vad) {
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;
msg.use_vad = use_vad;
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) {
@ -917,6 +932,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;
@ -941,7 +1014,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();
@ -978,6 +1051,12 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.manufacturer = "Espressif"; resp.manufacturer = "Espressif";
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
resp.manufacturer = "Raspberry Pi"; resp.manufacturer = "Raspberry Pi";
#elif defined(USE_BK72XX)
resp.manufacturer = "Beken";
#elif defined(USE_RTL87XX)
resp.manufacturer = "Realtek";
#elif defined(USE_HOST)
resp.manufacturer = "Host";
#endif #endif
resp.model = ESPHOME_BOARD; resp.model = ESPHOME_BOARD;
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
@ -991,12 +1070,11 @@ 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 = 1; resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version();
#endif #endif
return resp; return resp;
} }

View file

@ -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, bool use_vad);
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

File diff suppressed because it is too large Load diff

View file

@ -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,15 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage {
class VoiceAssistantRequest : public ProtoMessage { class VoiceAssistantRequest : public ProtoMessage {
public: public:
bool start{false}; bool start{false};
std::string conversation_id{};
bool use_vad{false};
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 +1702,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

View file

@ -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

View file

@ -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

View file

@ -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, bool use_vad) {
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
if (c->request_voice_assistant(true)) if (c->request_voice_assistant(true, conversation_id, use_vad))
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, "", 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

View file

@ -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, bool use_vad);
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 {

View file

@ -47,7 +47,7 @@ async def async_run_logs(config, address):
except APIConnectionError: except APIConnectionError:
cli.disconnect() cli.disconnect()
async def on_disconnect(): async def on_disconnect(expected_disconnect: bool) -> None:
_LOGGER.warning("Disconnected from API") _LOGGER.warning("Disconnected from API")
zc = zeroconf.Zeroconf() zc = zeroconf.Zeroconf()

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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_;

View file

@ -31,12 +31,10 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config): async def to_code(config):
hub = await cg.get_variable(config[CONF_AS3935_ID]) hub = await cg.get_variable(config[CONF_AS3935_ID])
if CONF_DISTANCE in config: if distance_config := config.get(CONF_DISTANCE):
conf = config[CONF_DISTANCE] sens = await sensor.new_sensor(distance_config)
distance_sensor = await sensor.new_sensor(conf) cg.add(hub.set_distance_sensor(sens))
cg.add(hub.set_distance_sensor(distance_sensor))
if CONF_LIGHTNING_ENERGY in config: if lightning_energy_config := config.get(CONF_LIGHTNING_ENERGY):
conf = config[CONF_LIGHTNING_ENERGY] sens = await sensor.new_sensor(lightning_energy_config)
lightning_energy_sensor = await sensor.new_sensor(conf) cg.add(hub.set_energy_sensor(sens))
cg.add(hub.set_energy_sensor(lightning_energy_sensor))

View file

@ -107,6 +107,6 @@ async def to_code(config):
cg.add(var.set_astep(config[CONF_ASTEP])) cg.add(var.set_astep(config[CONF_ASTEP]))
for conf_id, set_sensor_func in SENSORS.items(): for conf_id, set_sensor_func in SENSORS.items():
if conf_id in config: if sens_config := config.get(conf_id):
sens = await sensor.new_sensor(config[conf_id]) sens = await sensor.new_sensor(sens_config)
cg.add(getattr(var, set_sensor_func)(sens)) cg.add(getattr(var, set_sensor_func)(sens))

View file

@ -8,15 +8,15 @@ CODEOWNERS = ["@OttoWinter"]
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema({}), cv.Schema({}),
cv.only_with_arduino, cv.only_with_arduino,
cv.only_on(["esp32", "esp8266"]), cv.only_on(["esp32", "esp8266", "bk72xx", "rtl87xx"]),
) )
@coroutine_with_priority(200.0) @coroutine_with_priority(200.0)
async def to_code(config): async def to_code(config):
if CORE.is_esp32: if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json # https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") cg.add_library("esphome/AsyncTCP-esphome", "2.0.1")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP # https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3") cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")

View file

@ -83,18 +83,18 @@ async def to_code(config):
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if CONF_TEMPERATURE in config: if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature(sens)) cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config: if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config[CONF_HUMIDITY]) sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity(sens)) cg.add(var.set_humidity(sens))
if CONF_BATTERY_LEVEL in config: if battery_level_config := config.get(CONF_BATTERY_LEVEL):
sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) sens = await sensor.new_sensor(battery_level_config)
cg.add(var.set_battery_level(sens)) cg.add(var.set_battery_level(sens))
if CONF_BATTERY_VOLTAGE in config: if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) sens = await sensor.new_sensor(battery_voltage_config)
cg.add(var.set_battery_voltage(sens)) cg.add(var.set_battery_voltage(sens))
if CONF_SIGNAL_STRENGTH in config: if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
sens = await sensor.new_sensor(config[CONF_SIGNAL_STRENGTH]) sens = await sensor.new_sensor(signal_strength_config)
cg.add(var.set_signal_strength(sens)) cg.add(var.set_signal_strength(sens))

View file

@ -0,0 +1 @@
CODEOWNERS = ["@danieltwagner"]

View file

@ -0,0 +1,235 @@
#include "atm90e26.h"
#include "atm90e26_reg.h"
#include "esphome/core/log.h"
namespace esphome {
namespace atm90e26 {
static const char *const TAG = "atm90e26";
void ATM90E26Component::update() {
if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) {
this->status_set_warning();
return;
}
if (this->voltage_sensor_ != nullptr) {
this->voltage_sensor_->publish_state(this->get_line_voltage_());
}
if (this->current_sensor_ != nullptr) {
this->current_sensor_->publish_state(this->get_line_current_());
}
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(this->get_active_power_());
}
if (this->reactive_power_sensor_ != nullptr) {
this->reactive_power_sensor_->publish_state(this->get_reactive_power_());
}
if (this->power_factor_sensor_ != nullptr) {
this->power_factor_sensor_->publish_state(this->get_power_factor_());
}
if (this->forward_active_energy_sensor_ != nullptr) {
this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_());
}
if (this->reverse_active_energy_sensor_ != nullptr) {
this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_());
}
if (this->freq_sensor_ != nullptr) {
this->freq_sensor_->publish_state(this->get_frequency_());
}
this->status_clear_warning();
}
void ATM90E26Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component...");
this->spi_setup();
uint16_t mmode = 0x422; // default values for everything but L/N line current gains
mmode |= (gain_pga_ & 0x7) << 13;
mmode |= (n_line_gain_ & 0x3) << 11;
this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
this->write16_(ATM90E26_REGISTER_FUNCEN,
0x0030); // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0
uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA);
if (read != 0x0030) {
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read);
this->mark_failed();
return;
}
// TODO: 100 * <nominal voltage, e.g. 230> * sqrt(2) * <fraction of nominal, e.g. 0.9> / (4 * gain_voltage/32768)
this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD); // Voltage sag threshhold 0x1F2F
// Set metering calibration values
this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678); // CAL Metering calibration startup command
// Configure
this->write16_(ATM90E26_REGISTER_MMODE, mmode); // Metering Mode Configuration (see above)
this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16)); // PL Constant MSB
this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF); // PL Constant LSB
// Calibrate this to be 1 pulse per Wh
this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_); // L Line Calibration Gain (active power metering)
this->write16_(ATM90E26_REGISTER_LPHI, 0x0000); // L Line Calibration Angle
this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000); // N Line Calibration Gain
this->write16_(ATM90E26_REGISTER_NPHI, 0x0000); // N Line Calibration Angle
this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD); // Active Startup Power Threshold (default) = 2237
this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000); // Active No-Load Power Threshold
this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold (default) = 2796
this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000); // Reactive No-Load Power Threshold
// Compute Checksum for the registers we set above
// low byte = sum of all bytes
uint16_t cs =
((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) +
(pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) &
0xFF;
// high byte = XOR of all bytes
cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^
(pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC)
<< 8;
this->write16_(ATM90E26_REGISTER_CS1, cs);
ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs);
// Set measurement calibration values
this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678); // Measurement calibration startup command, registers 31-3A
this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_); // Voltage RMS gain
this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_); // L line current RMS gain
this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530); // N Line Current RMS Gain
this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000); // Voltage Offset
this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000); // L Line Current Offset
this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000); // N Line Current Offse
this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000); // L Line Active Power Offset
this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000); // L Line Reactive Power Offset
this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000); // N Line Active Power Offset
this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000); // N Line Reactive Power Offset
// Compute Checksum for the registers we set above
cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF;
cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8;
this->write16_(ATM90E26_REGISTER_CS2, cs);
ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs);
this->write16_(ATM90E26_REGISTER_CALSTART,
0x8765); // Checks correctness of 21-2B registers and starts normal metering if ok
this->write16_(ATM90E26_REGISTER_ADJSTART,
0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok
uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS);
if (sys_status & 0xC000) { // Checksum 1 Error
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X",
this->read16_(ATM90E26_REGISTER_CS1));
this->mark_failed();
}
if (sys_status & 0x3000) { // Checksum 2 Error
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X",
this->read16_(ATM90E26_REGISTER_CS2));
this->mark_failed();
}
}
void ATM90E26Component::dump_config() {
ESP_LOGCONFIG("", "ATM90E26:");
LOG_PIN(" CS Pin: ", this->cs_);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with ATM90E26 failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_);
LOG_SENSOR(" ", "Current A", this->current_sensor_);
LOG_SENSOR(" ", "Power A", this->power_sensor_);
LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_sensor_);
LOG_SENSOR(" ", "PF A", this->power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy A", this->forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
}
float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; }
uint16_t ATM90E26Component::read16_(uint8_t a_register) {
uint8_t data[2];
uint16_t output;
this->enable();
delayMicroseconds(4);
this->write_byte(a_register | 0x80);
delayMicroseconds(4);
this->read_array(data, 2);
this->disable();
output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output);
return output;
}
void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) {
ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val);
this->enable();
delayMicroseconds(4);
this->write_byte(a_register & 0x7F);
delayMicroseconds(4);
this->write_byte((val >> 8) & 0xFF);
this->write_byte(val & 0xFF);
this->disable();
}
float ATM90E26Component::get_line_current_() {
uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS);
return current / 1000.0f;
}
float ATM90E26Component::get_line_voltage_() {
uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS);
return voltage / 100.0f;
}
float ATM90E26Component::get_active_power_() {
int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement
return (float) val;
}
float ATM90E26Component::get_reactive_power_() {
int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement
return (float) val;
}
float ATM90E26Component::get_power_factor_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
if (val & 0x8000) {
return -(val & 0x7FF) / 1000.0f;
} else {
return val / 1000.0f;
}
}
float ATM90E26Component::get_forward_active_energy_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY);
if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) {
this->cumulative_forward_active_energy_ += val;
} else {
this->cumulative_forward_active_energy_ = val;
}
// The register holds thenths of pulses, we want to output Wh
return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_);
}
float ATM90E26Component::get_reverse_active_energy_() {
uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY);
if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) {
this->cumulative_reverse_active_energy_ += val;
} else {
this->cumulative_reverse_active_energy_ = val;
}
return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_);
}
float ATM90E26Component::get_frequency_() {
uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ);
return freq / 100.0f;
}
} // namespace atm90e26
} // namespace esphome

View file

@ -0,0 +1,72 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace atm90e26 {
class ATM90E26Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; }
void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; }
void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; }
void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; }
void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; }
void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; }
void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; }
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_line_freq(int freq) { line_freq_ = freq; }
void set_meter_constant(float val) { meter_constant_ = val; }
void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; }
void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; }
void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; }
void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; }
void set_gain_pga(uint16_t gain) { gain_pga_ = gain; }
void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; }
protected:
uint16_t read16_(uint8_t a_register);
int read32_(uint8_t addr_h, uint8_t addr_l);
void write16_(uint8_t a_register, uint16_t val);
float get_line_voltage_();
float get_line_current_();
float get_active_power_();
float get_reactive_power_();
float get_power_factor_();
float get_forward_active_energy_();
float get_reverse_active_energy_();
float get_frequency_();
float get_chip_temperature_();
sensor::Sensor *freq_sensor_{nullptr};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
sensor::Sensor *forward_active_energy_sensor_{nullptr};
sensor::Sensor *reverse_active_energy_sensor_{nullptr};
uint32_t cumulative_forward_active_energy_{0};
uint32_t cumulative_reverse_active_energy_{0};
uint16_t gain_metering_{7481};
uint16_t gain_voltage_{26400};
uint16_t gain_ct_{31251};
uint16_t gain_pga_{0x4};
uint16_t n_line_gain_{0x2};
int line_freq_{60};
float meter_constant_{3200.0f};
uint32_t pl_const_{1429876};
};
} // namespace atm90e26
} // namespace esphome

View file

@ -0,0 +1,70 @@
#pragma once
namespace esphome {
namespace atm90e26 {
/* Status and Special Register */
static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset
static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01; // System Status
static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02; // Function Enable
static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03; // Voltage Sag Threshold
static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04; // Small-Power Mode
static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06; // Last Read/Write SPI/UART Value
/* Metering Calibration and Configuration Register */
static const uint8_t ATM90E26_REGISTER_LSB = 0x08; // RMS/Power 16-bit LSB
static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20; // Calibration Start Command
static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21; // High Word of PL_Constant
static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22; // Low Word of PL_Constant
static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23; // L Line Calibration Gain
static const uint8_t ATM90E26_REGISTER_LPHI = 0x24; // L Line Calibration Angle
static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25; // N Line Calibration Gain
static const uint8_t ATM90E26_REGISTER_NPHI = 0x26; // N Line Calibration Angle
static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27; // Active Startup Power Threshold
static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28; // Active No-Load Power Threshold
static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29; // Reactive Startup Power Threshold
static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A; // Reactive No-Load Power Threshold
static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B; // Metering Mode Configuration
static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C; // Checksum 1
/* Measurement Calibration Register */
static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30; // Measurement Calibration Start Command
static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31; // Voltage RMS Gain
static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32; // L Line Current RMS Gain
static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33; // N Line Current RMS Gain
static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34; // Voltage Offset
static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35; // L Line Current Offset
static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36; // N Line Current Offse
static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37; // L Line Active Power Offset
static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38; // L Line Reactive Power Offset
static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39; // N Line Active Power Offset
static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A; // N Line Reactive Power Offset
static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B; // Checksum 2
/* Energy Register */
static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40; // Forward Active Energy
static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41; // Reverse Active Energy
static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42; // Absolute Active Energy
static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43; // Forward (Inductive) Reactive Energy
static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44; // Reverse (Capacitive) Reactive Energy
static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45; // Absolute Reactive Energy
static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46; // Metering Status
/* Measurement Register */
static const uint8_t ATM90E26_REGISTER_IRMS = 0x48; // L Line Current RMS
static const uint8_t ATM90E26_REGISTER_URMS = 0x49; // Voltage RMS
static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A; // L Line Mean Active Power
static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B; // L Line Mean Reactive Power
static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C; // Voltage Frequency
static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D; // L Line Power Factor
static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E; // Phase Angle between Voltage and L Line Current
static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F; // L Line Mean Apparent Power
static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68; // N Line Current rms
static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A; // N Line Mean Active Power
static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B; // N Line Mean Reactive Power
static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor
static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current
static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power
} // namespace atm90e26
} // namespace esphome

View file

@ -0,0 +1,157 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, spi
from esphome.const import (
CONF_ID,
CONF_REACTIVE_POWER,
CONF_VOLTAGE,
CONF_CURRENT,
CONF_POWER,
CONF_POWER_FACTOR,
CONF_FREQUENCY,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
ICON_LIGHTBULB,
ICON_CURRENT_AC,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT_HOURS,
)
CONF_LINE_FREQUENCY = "line_frequency"
CONF_METER_CONSTANT = "meter_constant"
CONF_PL_CONST = "pl_const"
CONF_GAIN_PGA = "gain_pga"
CONF_GAIN_METERING = "gain_metering"
CONF_GAIN_VOLTAGE = "gain_voltage"
CONF_GAIN_CT = "gain_ct"
LINE_FREQS = {
"50HZ": 50,
"60HZ": 60,
}
PGA_GAINS = {
"1X": 0x4,
"4X": 0x0,
"8X": 0x1,
"16X": 0x2,
"24X": 0x3,
}
atm90e26_ns = cg.esphome_ns.namespace("atm90e26")
ATM90E26Component = atm90e26_ns.class_(
"ATM90E26Component", cg.PollingComponent, spi.SPIDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ATM90E26Component),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
icon=ICON_LIGHTBULB,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
cv.Required(CONF_METER_CONSTANT): cv.positive_float,
cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t,
cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t,
cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range(
min=0, max=32767
),
cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t,
cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema())
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
if voltage_config := config.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(sens))
if current_config := config.get(CONF_CURRENT):
sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(sens))
if power_config := config.get(CONF_POWER):
sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(sens))
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(sens))
if power_factor_config := config.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(sens))
if forward_active_energy_config := config.get(CONF_FORWARD_ACTIVE_ENERGY):
sens = await sensor.new_sensor(forward_active_energy_config)
cg.add(var.set_forward_active_energy_sensor(sens))
if reverse_active_energy_config := config.get(CONF_REVERSE_ACTIVE_ENERGY):
sens = await sensor.new_sensor(reverse_active_energy_config)
cg.add(var.set_reverse_active_energy_sensor(sens))
if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT]))
cg.add(var.set_pl_const(config[CONF_PL_CONST]))
cg.add(var.set_gain_metering(config[CONF_GAIN_METERING]))
cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE]))
cg.add(var.set_gain_ct(config[CONF_GAIN_CT]))
cg.add(var.set_gain_pga(config[CONF_GAIN_PGA]))

View file

@ -6,6 +6,9 @@ from esphome.const import (
CONF_REACTIVE_POWER, CONF_REACTIVE_POWER,
CONF_VOLTAGE, CONF_VOLTAGE,
CONF_CURRENT, CONF_CURRENT,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_POWER, CONF_POWER,
CONF_POWER_FACTOR, CONF_POWER_FACTOR,
CONF_FREQUENCY, CONF_FREQUENCY,
@ -31,10 +34,6 @@ from esphome.const import (
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
CONF_PHASE_A = "phase_a"
CONF_PHASE_B = "phase_b"
CONF_PHASE_C = "phase_c"
CONF_LINE_FREQUENCY = "line_frequency" CONF_LINE_FREQUENCY = "line_frequency"
CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_CHIP_TEMPERATURE = "chip_temperature"
CONF_GAIN_PGA = "gain_pga" CONF_GAIN_PGA = "gain_pga"
@ -151,33 +150,35 @@ async def to_code(config):
conf = config[phase] conf = config[phase]
cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
if CONF_VOLTAGE in conf: if voltage_config := conf.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(conf[CONF_VOLTAGE]) sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(i, sens)) cg.add(var.set_voltage_sensor(i, sens))
if CONF_CURRENT in conf: if current_config := conf.get(CONF_CURRENT):
sens = await sensor.new_sensor(conf[CONF_CURRENT]) sens = await sensor.new_sensor(current_config)
cg.add(var.set_current_sensor(i, sens)) cg.add(var.set_current_sensor(i, sens))
if CONF_POWER in conf: if power_config := conf.get(CONF_POWER):
sens = await sensor.new_sensor(conf[CONF_POWER]) sens = await sensor.new_sensor(power_config)
cg.add(var.set_power_sensor(i, sens)) cg.add(var.set_power_sensor(i, sens))
if CONF_REACTIVE_POWER in conf: if reactive_power_config := conf.get(CONF_REACTIVE_POWER):
sens = await sensor.new_sensor(conf[CONF_REACTIVE_POWER]) sens = await sensor.new_sensor(reactive_power_config)
cg.add(var.set_reactive_power_sensor(i, sens)) cg.add(var.set_reactive_power_sensor(i, sens))
if CONF_POWER_FACTOR in conf: if power_factor_config := conf.get(CONF_POWER_FACTOR):
sens = await sensor.new_sensor(conf[CONF_POWER_FACTOR]) sens = await sensor.new_sensor(power_factor_config)
cg.add(var.set_power_factor_sensor(i, sens)) cg.add(var.set_power_factor_sensor(i, sens))
if CONF_FORWARD_ACTIVE_ENERGY in conf: if forward_active_energy_config := conf.get(CONF_FORWARD_ACTIVE_ENERGY):
sens = await sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY]) sens = await sensor.new_sensor(forward_active_energy_config)
cg.add(var.set_forward_active_energy_sensor(i, sens)) cg.add(var.set_forward_active_energy_sensor(i, sens))
if CONF_REVERSE_ACTIVE_ENERGY in conf: if reverse_active_energy_config := conf.get(CONF_REVERSE_ACTIVE_ENERGY):
sens = await sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY]) sens = await sensor.new_sensor(reverse_active_energy_config)
cg.add(var.set_reverse_active_energy_sensor(i, sens)) cg.add(var.set_reverse_active_energy_sensor(i, sens))
if CONF_FREQUENCY in config:
sens = await sensor.new_sensor(config[CONF_FREQUENCY]) if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens)) cg.add(var.set_freq_sensor(sens))
if CONF_CHIP_TEMPERATURE in config: if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) sens = await sensor.new_sensor(chip_temperature_config)
cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_chip_temperature_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))

View file

@ -87,6 +87,6 @@ async def to_code(config):
(CONF_MOISTURE, var.set_soil_moisture), (CONF_MOISTURE, var.set_soil_moisture),
(CONF_ILLUMINANCE, var.set_illuminance), (CONF_ILLUMINANCE, var.set_illuminance),
]: ]:
if config_key in config: if sensor_config := config.get(config_key):
sens = await sensor.new_sensor(config[config_key]) sens = await sensor.new_sensor(sensor_config)
cg.add(setter(sens)) cg.add(setter(sens))

View file

@ -57,19 +57,18 @@ async def to_code(config):
var.get_idle_trigger(), [], config[CONF_IDLE_ACTION] var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]
) )
if CONF_COOL_ACTION in config: if cool_action_config := config.get(CONF_COOL_ACTION):
await automation.build_automation( await automation.build_automation(
var.get_cool_trigger(), [], config[CONF_COOL_ACTION] var.get_cool_trigger(), [], cool_action_config
) )
cg.add(var.set_supports_cool(True)) cg.add(var.set_supports_cool(True))
if CONF_HEAT_ACTION in config: if heat_action_config := config.get(CONF_HEAT_ACTION):
await automation.build_automation( await automation.build_automation(
var.get_heat_trigger(), [], config[CONF_HEAT_ACTION] var.get_heat_trigger(), [], heat_action_config
) )
cg.add(var.set_supports_heat(True)) cg.add(var.set_supports_heat(True))
if CONF_AWAY_CONFIG in config: if away := config.get(CONF_AWAY_CONFIG):
away = config[CONF_AWAY_CONFIG]
away_config = BangBangClimateTargetTempConfig( away_config = BangBangClimateTargetTempConfig(
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],

View file

@ -45,8 +45,8 @@ 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 cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
if CONF_TIME_ID in config: if time_id := config.get(CONF_TIME_ID):
time_ = await cg.get_variable(config[CONF_TIME_ID]) time_ = await cg.get_variable(time_id)
cg.add(var.set_time_id(time_)) cg.add(var.set_time_id(time_))
if CONF_RECEIVE_TIMEOUT in config: if (receive_timeout := config.get(CONF_RECEIVE_TIMEOUT)) is not None:
cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) cg.add(var.set_status_timeout(receive_timeout))

View file

@ -3,6 +3,7 @@
#include "bedjet_hub.h" #include "bedjet_hub.h"
#include "bedjet_child.h" #include "bedjet_child.h"
#include "bedjet_const.h" #include "bedjet_const.h"
#include <cinttypes>
namespace esphome { namespace esphome {
namespace bedjet { namespace bedjet {
@ -373,7 +374,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
// Set reentrant flag to prevent processing multiple packets. // Set reentrant flag to prevent processing multiple packets.
this->processing_ = true; this->processing_ = true;
ESP_LOGVV(TAG, "[%s] Decoding packet: last=%d, delta=%d, force=%s", this->get_name().c_str(), ESP_LOGVV(TAG, "[%s] Decoding packet: last=%" PRId32 ", delta=%" PRId32 ", force=%s", this->get_name().c_str(),
this->last_notify_, delta, this->force_refresh_ ? "y" : "n"); this->last_notify_, delta, this->force_refresh_ ? "y" : "n");
bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
@ -442,7 +443,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);
@ -523,11 +524,11 @@ void BedJetHub::dispatch_status_() {
ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
} else if (diff > NOTIFY_WARN_THRESHOLD) { } else if (diff > NOTIFY_WARN_THRESHOLD) {
ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); ESP_LOGW(TAG, "[%s] Last GATT notify was %" PRId32 " seconds ago.", this->get_name().c_str(), diff / 1000);
} }
if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); ESP_LOGW(TAG, "[%s] Timed out after %" PRId32 " sec. Retrying...", this->get_name().c_str(), this->timeout_);
// set_enabled(false) will only close the connection if state != IDLE. // set_enabled(false) will only close the connection if state != IDLE.
this->parent()->set_state(espbt::ClientState::CONNECTING); this->parent()->set_state(espbt::ClientState::CONNECTING);
this->parent()->set_enabled(false); this->parent()->set_enabled(false);

View file

@ -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>

View file

@ -29,10 +29,10 @@ async def to_code(config):
output_ = await cg.get_variable(config[CONF_OUTPUT]) output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_)) cg.add(var.set_output(output_))
if CONF_OSCILLATION_OUTPUT in config: if oscillation_output_id := config.get(CONF_OSCILLATION_OUTPUT):
oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) oscillation_output = await cg.get_variable(oscillation_output_id)
cg.add(var.set_oscillating(oscillation_output)) cg.add(var.set_oscillating(oscillation_output))
if CONF_DIRECTION_OUTPUT in config: if direction_output_id := config.get(CONF_DIRECTION_OUTPUT):
direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) direction_output = await cg.get_variable(direction_output_id)
cg.add(var.set_direction(direction_output)) cg.add(var.set_direction(direction_output))

View file

@ -95,6 +95,14 @@ DEVICE_CLASSES = [
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_TIME_OFF = "time_off"
CONF_TIME_ON = "time_on"
DEFAULT_DELAY = "1s"
DEFAULT_TIME_OFF = "100ms"
DEFAULT_TIME_ON = "900ms"
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor") binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase) BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
BinarySensorInitiallyOff = binary_sensor_ns.class_( BinarySensorInitiallyOff = binary_sensor_ns.class_(
@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@FILTER_REGISTRY.register("invert", InvertFilter, {}) def register_filter(name, filter_type, schema):
return FILTER_REGISTRY.register(name, filter_type, schema)
@register_filter("invert", InvertFilter, {})
async def invert_filter_to_code(config, filter_id): async def invert_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id) return cg.new_Pvariable(filter_id)
@FILTER_REGISTRY.register( @register_filter(
"delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds "delayed_on_off",
DelayedOnOffFilter,
cv.Any(
cv.templatable(cv.positive_time_period_milliseconds),
cv.Schema(
{
cv.Required(CONF_TIME_ON): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Required(CONF_TIME_OFF): cv.templatable(
cv.positive_time_period_milliseconds
),
}
),
msg="'delayed_on_off' filter requires either a delay time to be used for both "
"turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if "
"different delay times are required.",
),
) )
async def delayed_on_off_filter_to_code(config, filter_id): async def delayed_on_off_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id, config) var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {}) await cg.register_component(var, {})
if isinstance(config, dict):
template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32)
cg.add(var.set_on_delay(template_))
template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32)
cg.add(var.set_off_delay(template_))
else:
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_on_delay(template_))
cg.add(var.set_off_delay(template_))
return var return var
@FILTER_REGISTRY.register( @register_filter(
"delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds "delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
) )
async def delayed_on_filter_to_code(config, filter_id): async def delayed_on_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id, config) var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {}) await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var return var
@FILTER_REGISTRY.register( @register_filter(
"delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds "delayed_off",
DelayedOffFilter,
cv.templatable(cv.positive_time_period_milliseconds),
) )
async def delayed_off_filter_to_code(config, filter_id): async def delayed_off_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id, config) var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {}) await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var return var
CONF_TIME_OFF = "time_off" @register_filter(
CONF_TIME_ON = "time_on"
DEFAULT_DELAY = "1s"
DEFAULT_TIME_OFF = "100ms"
DEFAULT_TIME_ON = "900ms"
@FILTER_REGISTRY.register(
"autorepeat", "autorepeat",
AutorepeatFilter, AutorepeatFilter,
cv.All( cv.All(
@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id):
return var return var
@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda) @register_filter("lambda", LambdaFilter, cv.returning_lambda)
async def lambda_filter_to_code(config, filter_id): async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(bool, "x")], return_type=cg.optional.template(bool) config, [(bool, "x")], return_type=cg.optional.template(bool)
@ -323,6 +359,18 @@ def validate_multi_click_timing(value):
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
def validate_click_timing(value):
for v in value:
min_length = v.get(CONF_MIN_LENGTH)
max_length = v.get(CONF_MAX_LENGTH)
if max_length < min_length:
raise cv.Invalid(
f"Max length ({max_length}) must be larger than min length ({min_length})."
)
return value
BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
{ {
cv.GenerateID(): cv.declare_id(BinarySensor), cv.GenerateID(): cv.declare_id(BinarySensor),
@ -342,27 +390,33 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
} }
), ),
cv.Optional(CONF_ON_CLICK): automation.validate_automation( cv.Optional(CONF_ON_CLICK): cv.All(
{ automation.validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), {
cv.Optional( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
CONF_MIN_LENGTH, default="50ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_MIN_LENGTH, default="50ms"
cv.Optional( ): cv.positive_time_period_milliseconds,
CONF_MAX_LENGTH, default="350ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_MAX_LENGTH, default="350ms"
} ): cv.positive_time_period_milliseconds,
}
),
validate_click_timing,
), ),
cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation( cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
{ automation.validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), {
cv.Optional( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
CONF_MIN_LENGTH, default="50ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_MIN_LENGTH, default="50ms"
cv.Optional( ): cv.positive_time_period_milliseconds,
CONF_MAX_LENGTH, default="350ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_MAX_LENGTH, default="350ms"
} ): cv.positive_time_period_milliseconds,
}
),
validate_click_timing,
), ),
cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
{ {
@ -413,14 +467,14 @@ def binary_sensor_schema(
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
if CONF_DEVICE_CLASS in config: if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) cg.add(var.set_device_class(device_class))
if CONF_PUBLISH_INITIAL_STATE in config: if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE):
cg.add(var.set_publish_initial_state(config[CONF_PUBLISH_INITIAL_STATE])) cg.add(var.set_publish_initial_state(publish_initial_state))
if CONF_INVERTED in config: if inverted := config.get(CONF_INVERTED):
cg.add(var.set_inverted(config[CONF_INVERTED])) cg.add(var.set_inverted(inverted))
if CONF_FILTERS in config: if filters_config := config.get(CONF_FILTERS):
filters = await cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS]) filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
cg.add(var.add_filters(filters)) cg.add(var.add_filters(filters))
for conf in config.get(CONF_ON_PRESS, []): for conf in config.get(CONF_ON_PRESS, []):
@ -464,8 +518,8 @@ async def setup_binary_sensor_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf) await automation.build_automation(trigger, [(bool, "x")], conf)
if CONF_MQTT_ID in config: if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config) await mqtt.register_mqtt_component(mqtt_, config)

View file

@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) {
} }
} }
DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
} else { } else {
this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
} }
return {}; return {};
} }
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout("ON");
@ -51,10 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {}
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout("OFF");
@ -114,15 +111,6 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> UniqueFilter::new_value(bool value, bool is_initial) {
if (this->last_value_.has_value() && *this->last_value_ == value) {
return {};
} else {
this->last_value_ = value;
return value;
}
}
} // namespace binary_sensor } // namespace binary_sensor
} // namespace esphome } // namespace esphome

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@ -29,38 +30,40 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component { class DelayedOnOffFilter : public Filter, public Component {
public: public:
explicit DelayedOnOffFilter(uint32_t delay);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; }
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
protected: protected:
uint32_t delay_; TemplatableValue<uint32_t> on_delay_{};
TemplatableValue<uint32_t> off_delay_{};
}; };
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
public: public:
explicit DelayedOnFilter(uint32_t delay);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected: protected:
uint32_t delay_; TemplatableValue<uint32_t> delay_{};
}; };
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
public: public:
explicit DelayedOffFilter(uint32_t delay);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected: protected:
uint32_t delay_; TemplatableValue<uint32_t> delay_{};
}; };
class InvertFilter : public Filter { class InvertFilter : public Filter {
@ -105,14 +108,6 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
}; };
class UniqueFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
protected:
optional<bool> last_value_{};
};
} // namespace binary_sensor } // namespace binary_sensor
} // namespace esphome } // namespace esphome

View file

@ -0,0 +1,51 @@
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_BK72XX,
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
)
from esphome.core import CORE
from .boards import BK72XX_BOARDS, BK72XX_BOARD_PINS
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"]
COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_BK72XX,
boards=BK72XX_BOARDS,
board_pins=BK72XX_BOARD_PINS,
pin_validation=None,
usage_validation=None,
)
def _set_core_data(config):
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
CONFIG_SCHEMA = libretiny.BASE_SCHEMA
PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)

File diff suppressed because it is too large Load diff

View file

@ -93,35 +93,27 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
if CONF_VOLTAGE in config: if voltage_config := config.get(CONF_VOLTAGE):
conf = config[CONF_VOLTAGE] sens = await sensor.new_sensor(voltage_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_voltage_sensor(sens)) cg.add(var.set_voltage_sensor(sens))
if CONF_CURRENT_1 in config: if current_1_config := config.get(CONF_CURRENT_1):
conf = config[CONF_CURRENT_1] sens = await sensor.new_sensor(current_1_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_current_sensor_1(sens)) cg.add(var.set_current_sensor_1(sens))
if CONF_CURRENT_2 in config: if current_2_config := config.get(CONF_CURRENT_2):
conf = config[CONF_CURRENT_2] sens = await sensor.new_sensor(current_2_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_current_sensor_2(sens)) cg.add(var.set_current_sensor_2(sens))
if CONF_ACTIVE_POWER_1 in config: if active_power_1_config := config.get(CONF_ACTIVE_POWER_1):
conf = config[CONF_ACTIVE_POWER_1] sens = await sensor.new_sensor(active_power_1_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_power_sensor_1(sens)) cg.add(var.set_power_sensor_1(sens))
if CONF_ACTIVE_POWER_2 in config: if active_power_2_config := config.get(CONF_ACTIVE_POWER_2):
conf = config[CONF_ACTIVE_POWER_2] sens = await sensor.new_sensor(active_power_2_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_power_sensor_2(sens)) cg.add(var.set_power_sensor_2(sens))
if CONF_ENERGY_1 in config: if energy_1_config := config.get(CONF_ENERGY_1):
conf = config[CONF_ENERGY_1] sens = await sensor.new_sensor(energy_1_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor_1(sens)) cg.add(var.set_energy_sensor_1(sens))
if CONF_ENERGY_2 in config: if energy_2_config := config.get(CONF_ENERGY_2):
conf = config[CONF_ENERGY_2] sens = await sensor.new_sensor(energy_2_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor_2(sens)) cg.add(var.set_energy_sensor_2(sens))
if CONF_ENERGY_TOTAL in config: if energy_total_config := config.get(CONF_ENERGY_TOTAL):
conf = config[CONF_ENERGY_TOTAL] sens = await sensor.new_sensor(energy_total_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor_sum(sens)) cg.add(var.set_energy_sensor_sum(sens))

View file

@ -79,27 +79,21 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
if CONF_VOLTAGE in config: if voltage_config := config.get(CONF_VOLTAGE):
conf = config[CONF_VOLTAGE] sens = await sensor.new_sensor(voltage_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_voltage_sensor(sens)) cg.add(var.set_voltage_sensor(sens))
if CONF_CURRENT in config: if current_config := config.get(CONF_CURRENT):
conf = config[CONF_CURRENT] sens = await sensor.new_sensor(current_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_current_sensor(sens)) cg.add(var.set_current_sensor(sens))
if CONF_POWER in config: if power_config := config.get(CONF_POWER):
conf = config[CONF_POWER] sens = await sensor.new_sensor(power_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_power_sensor(sens)) cg.add(var.set_power_sensor(sens))
if CONF_ENERGY in config: if energy_config := config.get(CONF_ENERGY):
conf = config[CONF_ENERGY] sens = await sensor.new_sensor(energy_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_energy_sensor(sens)) cg.add(var.set_energy_sensor(sens))
if CONF_INTERNAL_TEMPERATURE in config: if internal_temperature_config := config.get(CONF_INTERNAL_TEMPERATURE):
conf = config[CONF_INTERNAL_TEMPERATURE] sens = await sensor.new_sensor(internal_temperature_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_internal_temperature_sensor(sens)) cg.add(var.set_internal_temperature_sensor(sens))
if CONF_EXTERNAL_TEMPERATURE in config: if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
conf = config[CONF_EXTERNAL_TEMPERATURE] sens = await sensor.new_sensor(external_temperature_config)
sens = await sensor.new_sensor(conf)
cg.add(var.set_external_temperature_sensor(sens)) cg.add(var.set_external_temperature_sensor(sens))

Some files were not shown because too many files have changed in this diff Show more