Merge remote-tracking branch 'remotes/origin/dev' into esp32_bt_classic_component

# Conflicts:
#	esphome/components/esp32_ble_beacon/__init__.py
#	esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
#	tests/test2.yaml
#	tests/test5.yaml
This commit is contained in:
RoboMagus 2024-11-16 16:17:26 +01:00
commit ec06744bd5
4631 changed files with 162181 additions and 26942 deletions

View file

@ -1,19 +1,37 @@
{
"name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev",
"postCreateCommand": ["script/devcontainer-post-create"],
"postCreateCommand": [
"script/devcontainer-post-create"
],
"containerEnv": {
"DEVCONTAINER": "1",
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
},
"runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"],
"runArgs": [
"--privileged",
"-e",
"ESPHOME_DASHBOARD_USE_PING=1"
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,
// if you are using avahi in the host device, uncomment these to allow the
// devcontainer to find devices via mdns
//"mounts": [
// "type=bind,source=/dev/bus/usb,target=/dev/bus/usb",
// "type=bind,source=/var/run/dbus,target=/var/run/dbus",
// "type=bind,source=/var/run/avahi-daemon/socket,target=/var/run/avahi-daemon/socket"
//],
"customizations": {
"vscode": {
"extensions": [
// python
"ms-python.python",
"ms-python.pylint",
"ms-python.flake8",
"ms-python.black-formatter",
"visualstudioexptteam.vscodeintellicode",
// yaml
"redhat.vscode-yaml",
@ -25,9 +43,21 @@
"settings": {
"python.languageServer": "Pylance",
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"pylint.args": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
"flake8.args": [
"--config=${workspaceFolder}/.flake8"
],
"black-formatter.args": [
"--config",
"${workspaceFolder}/pyproject.toml"
],
"[python]": {
// VS will say "Value is not accepted" before building the devcontainer, but the warning
// should go away after build is completed.
"editor.defaultFormatter": "ms-python.black-formatter"
},
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,

View file

@ -75,6 +75,9 @@ target/
# pyenv
.python-version
# asdf
.tool-versions
# celery beat schedule file
celerybeat-schedule

42
.flake8 Normal file
View file

@ -0,0 +1,42 @@
[flake8]
max-line-length = 120
# Following 4 for black compatibility
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# TODO fix flake8
# D100 Missing docstring in public module
# D101 Missing docstring in public class
# D102 Missing docstring in public method
# D103 Missing docstring in public function
# D104 Missing docstring in public package
# D105 Missing docstring in magic method
# D107 Missing docstring in __init__
# D200 One-line docstring should fit on one line with quotes
# D205 1 blank line required between summary line and description
# D209 Multi-line docstring closing quotes should be on a separate line
# D400 First line should end with a period
# D401 First line should be in imperative mood
ignore =
E501,
W503,
E203,
D202,
D100,
D101,
D102,
D103,
D104,
D105,
D107,
D200,
D205,
D209,
D400,
D401,
exclude = api_pb2.py

View file

@ -7,11 +7,16 @@
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
**Related issue or feature (if applicable):** fixes <link to issue>
**Related issue or feature (if applicable):**
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>
- fixes <link to issue>
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#<esphome-docs PR number goes here>
## Test Environment
@ -23,12 +28,6 @@
- [ ] RTL87xx
## Example entry for `config.yaml`:
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR. Furthermore, for new integrations, it gives an impression of how
the configuration would look like.
Note: Remove this section if this PR does not have an example entry.
-->
```yaml
# Example config.yaml

View file

@ -34,16 +34,29 @@ runs:
echo $l >> $GITHUB_OUTPUT
done
# set cache-to only if dev branch
- id: cache-to
shell: bash
run: |-
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "value=type=gha,mode=max" >> $GITHUB_OUTPUT
else
echo "value=" >> $GITHUB_OUTPUT
fi
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v5.0.0
uses: docker/build-push-action@v6.9.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: .
file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: ${{ steps.cache-to.outputs.value }}
build-args: |
BASEIMGTYPE=${{ inputs.baseimg }}
BUILD_VERSION=${{ inputs.version }}
@ -57,24 +70,19 @@ runs:
digest="${{ steps.build-ghcr.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}"
- name: Upload ghcr digest
uses: actions/upload-artifact@v3.1.3
with:
name: digests-${{ inputs.target }}-ghcr
path: /tmp/digests/${{ inputs.target }}/ghcr/*
if-no-files-found: error
retention-days: 1
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v5.0.0
uses: docker/build-push-action@v6.9.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: .
file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: ${{ steps.cache-to.outputs.value }}
build-args: |
BASEIMGTYPE=${{ inputs.baseimg }}
BUILD_VERSION=${{ inputs.version }}
@ -87,11 +95,3 @@ runs:
mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub
digest="${{ steps.build-dockerhub.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}"
- name: Upload dockerhub digest
uses: actions/upload-artifact@v3.1.3
with:
name: digests-${{ inputs.target }}-dockerhub
path: /tmp/digests/${{ inputs.target }}/dockerhub/*
if-no-files-found: error
retention-days: 1

View file

@ -17,22 +17,31 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v4.1.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'
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
shell: bash
run: |
python -m venv venv
. venv/bin/activate
source venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
shell: bash
run: |
python -m venv venv
./venv/Scripts/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .

View file

@ -13,3 +13,20 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
groups:
docker-actions:
applies-to: version-updates
patterns:
- "docker/setup-qemu-action"
- "docker/login-action"
- "docker/setup-buildx-action"
- package-ecosystem: github-actions
directory: "/.github/actions/build-image"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/.github/actions/restore-python"
schedule:
interval: daily
open-pull-requests-limit: 10

80
.github/workflows/ci-api-proto.yml vendored Normal file
View file

@ -0,0 +1,80 @@
name: API Proto CI
on:
pull_request:
paths:
- "esphome/components/api/api.proto"
- "esphome/components/api/api_pb2.cpp"
- "esphome/components/api/api_pb2.h"
- "esphome/components/api/api_pb2_service.cpp"
- "esphome/components/api/api_pb2_service.h"
- "script/api_protobuf/api_protobuf.py"
- ".github/workflows/ci-api-proto.yml"
permissions:
contents: read
pull-requests: write
jobs:
check:
name: Check generated files
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.11"
- name: Install apt dependencies
run: |
sudo apt update
sudo apt-cache show protobuf-compiler
sudo apt install -y protobuf-compiler
protoc --version
- name: Install python dependencies
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
- name: Generate files
run: script/api_protobuf/api_protobuf.py
- name: Check for changes
run: |
if ! git diff --quiet; then
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
echo "You have altered the generated proto files but they do not match what is expected." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/api_protobuf/api_protobuf.py' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
exit 1
fi
- if: failure()
name: Review PR
uses: actions/github-script@v7.0.1
with:
script: |
await github.rest.pulls.createReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
event: 'REQUEST_CHANGES',
body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
})
- if: success()
name: Dismiss review
uses: actions/github-script@v7.0.1
with:
script: |
let reviews = await github.rest.pulls.listReviews({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
for (let review of reviews.data) {
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
review_id: review.id,
message: 'Files now match the expected proto files.'
});
}
}

View file

@ -2,7 +2,7 @@
name: CI for docker images
# Only run when docker paths change
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -40,15 +40,15 @@ jobs:
arch: [amd64, armv7, aarch64]
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3.7.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set TAG
run: |

View file

@ -1,7 +1,6 @@
---
name: CI
# yamllint disable-line rule:truthy
on:
push:
branches: [dev, beta, release]
@ -10,7 +9,10 @@ on:
paths:
- "**"
- "!.github/workflows/*.yml"
- "!.github/actions/build-image/*"
- ".github/workflows/ci.yml"
- "!.yamllint"
- "!.github/dependabot.yml"
merge_group:
permissions:
@ -19,7 +21,6 @@ permissions:
env:
DEFAULT_PYTHON: "3.9"
PYUPGRADE_TARGET: "--py39-plus"
CLANG_FORMAT_VERSION: "13.0.1"
concurrency:
# yamllint disable-line rule:line-length
@ -34,18 +35,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- 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@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.2
uses: actions/cache@v4.1.2
with:
path: venv
# yamllint disable-line rule:line-length
@ -66,7 +67,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -87,7 +88,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -108,7 +109,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -129,7 +130,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -150,7 +151,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -166,23 +167,61 @@ jobs:
pytest:
name: Run pytest
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
os:
- ubuntu-latest
- macOS-latest
- windows-latest
exclude:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.12"
os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.9"
os: windows-latest
- python-version: "3.12"
os: macOS-latest
- python-version: "3.10"
os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
python-version: ${{ matrix.python-version }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run pytest
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native tests
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --tb=native tests
pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
clang-format:
name: Check clang-format
@ -191,7 +230,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@ -200,7 +239,7 @@ jobs:
- name: Install clang-format
run: |
. venv/bin/activate
pip install clang-format==${{ env.CLANG_FORMAT_VERSION }}
pip install clang-format -c requirements_dev.txt
- name: Run clang-format
run: |
. venv/bin/activate
@ -210,72 +249,6 @@ jobs:
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.1
- 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
validate-tests:
name: Validate YAML test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- compile-tests-list
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome config ${{ matrix.file }}
run: |
. venv/bin/activate
esphome config ${{ matrix.file }}
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
- validate-tests
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.compile-tests-list.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run esphome compile ${{ matrix.file }}
run: |
. venv/bin/activate
esphome compile ${{ matrix.file }}
clang-tidy:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
@ -320,27 +293,44 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache@v3.3.2
if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.1.2
with:
path: ~/.platformio
# yamllint disable-line rule:line-length
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
key: platformio-${{ matrix.pio_cache_key }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.1.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
- name: Install clang-tidy
run: sudo apt-get install clang-tidy-14
run: |
sudo apt-get update
sudo apt-get install clang-tidy-14
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Run 'pio run --list-targets -e esp32-idf-tidy'
if: matrix.name == 'Run script/clang-tidy for ESP32 IDF'
run: |
. venv/bin/activate
mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy
- name: Run clang-tidy
run: |
. venv/bin/activate
@ -354,6 +344,143 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
list-components:
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request'
outputs:
components: ${{ steps.list-components.outputs.components }}
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
- name: Get target branch
id: target-branch
run: |
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Find changed components
id: list-components
run: |
. venv/bin/activate
components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }})
output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
count=$(echo "$output_components" | jq length)
echo "components=$output_components" >> $GITHUB_OUTPUT
echo "count=$count" >> $GITHUB_OUTPUT
echo "$count Components:"
echo "$output_components" | jq
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }}
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: test_build_components -e config -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e config -c ${{ matrix.file }}
- name: test_build_components -e compile -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e compile -c ${{ matrix.file }}
test-build-components-splitter:
name: Split components for testing into 20 groups maximum
runs-on: ubuntu-latest
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Split components into 20 groups
id: split
run: |
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split:
name: Test split components
runs-on: ubuntu-latest
needs:
- common
- list-components
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
strategy:
fail-fast: false
max-parallel: 4
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps:
- name: List components
run: echo ${{ matrix.components }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config
run: |
. venv/bin/activate
for component in ${{ matrix.components }}; do
./script/test_build_components -e config -c $component
done
- name: Compile config
run: |
. venv/bin/activate
mkdir build_cache
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
for component in ${{ matrix.components }}; do
./script/test_build_components -e compile -c $component
done
ci-status:
name: CI Status
runs-on: ubuntu-latest
@ -366,8 +493,11 @@ jobs:
- pylint
- pytest
- pyupgrade
- compile-tests
- clang-tidy
- list-components
- test-build-components
- test-build-components-splitter
- test-build-components-split
if: always()
steps:
- name: Success
@ -375,4 +505,8 @@ jobs:
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
env:
JSON_DOC: ${{ toJSON(needs) }}
run: |
echo $JSON_DOC | jq
exit 1

91
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,91 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
workflow_dispatch:
schedule:
- cron: "30 18 * * 4"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
# - language: c-cpp
# build-mode: autobuild
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View file

@ -1,7 +1,6 @@
---
name: Lock
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "30 0 * * *"
@ -18,7 +17,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.1
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""

View file

@ -1,7 +1,6 @@
---
name: Publish Release
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
release:
@ -18,14 +17,16 @@ jobs:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }}
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.7
- name: Get tag
id: tag
# yamllint disable rule:line-length
run: |
if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then
TAG="${GITHUB_REF#refs/tags/}"
if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false"
else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')"
@ -33,34 +34,38 @@ jobs:
BRANCH=${GITHUB_REF#refs/heads/}
if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true"
else
BRANCH_BUILD="false"
fi
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length
deploy-pypi:
name: Build and publish to PyPi
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: "3.x"
- name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: |
script/setup
pip install twine
run: script/setup
- name: Build
run: python setup.py sdist bdist_wheel
- name: Upload
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload dist/*
run: |-
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.2
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}
@ -78,25 +83,25 @@ jobs:
- linux/arm/v7
- linux/arm64
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.7
- name: Set up Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3.7.1
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.2.0
- name: Log in to docker hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -129,6 +134,19 @@ jobs:
suffix: lint
version: ${{ needs.init.outputs.tag }}
- name: Sanitize platform name
id: sanitize
run: |
echo "${{ matrix.platform }}" | sed 's|/|-|g' > /tmp/platform
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests
uses: actions/upload-artifact@v4.4.3
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests
retention-days: 1
deploy-manifest:
name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }}
runs-on: ubuntu-latest
@ -156,24 +174,27 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/checkout@v4.1.7
- name: Download digests
uses: actions/download-artifact@v3.0.2
uses: actions/download-artifact@v4.1.8
with:
name: digests-${{ matrix.image.target }}-${{ matrix.registry }}
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3.7.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -192,28 +213,34 @@ jobs:
done
- name: Create manifest list and push
working-directory: /tmp/digests
working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }}
run: |
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
deploy-ha-addon-repo:
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [deploy-manifest]
needs:
- init
- deploy-manifest
steps:
- name: Trigger Workflow
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
description = ${{ toJSON(github.event.release.body) }};
}
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "home-assistant-addon",
workflow_id: "bump-version.yml",
ref: "main",
inputs: {
version: "${{ github.event.release.tag_name }}",
content: ${{ toJSON(github.event.release.body) }}
version: "${{ needs.init.outputs.tag }}",
content: description
}
})

View file

@ -1,7 +1,6 @@
---
name: Stale
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "30 0 * * *"
@ -18,7 +17,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8.0.0
- uses: actions/stale@v9.0.0
with:
days-before-pr-stale: 90
days-before-pr-close: 7
@ -38,7 +37,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8.0.0
- uses: actions/stale@v9.0.0
with:
days-before-pr-stale: -1
days-before-pr-close: -1

View file

@ -13,18 +13,18 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Checkout Home Assistant
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v5.3.0
with:
python-version: 3.11
python-version: 3.12
- name: Install Home Assistant
run: |
@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v5.0.2
uses: peter-evans/create-pull-request@v7.0.5
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View file

@ -1,3 +1,4 @@
---
name: YAML lint
on:
@ -17,6 +18,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.7
- name: Run yamllint
uses: frenck/action-yamllint@v1.4.1
uses: frenck/action-yamllint@v1.5.0
with:
strict: true

5
.gitignore vendored
View file

@ -75,6 +75,9 @@ cov.xml
# pyenv
.python-version
# asdf
.tool-versions
# Environments
.env
.venv
@ -138,3 +141,5 @@ sdkconfig.*
.tests/
/components
/managed_components

View file

@ -2,8 +2,17 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.4
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.11.0
rev: 24.4.2
hooks:
- id: black
args:
@ -27,7 +36,23 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.15.2
hooks:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1
hooks:
- id: clang-format
types_or: [c, c++]
- repo: local
hooks:
- id: pylint
name: pylint
entry: script/run-in-env.sh pylint
language: script
types: [python]

View file

@ -1,3 +1,19 @@
---
ignore: |
venv/
extends: default
ignore-from-file: .gitignore
rules:
document-start: disable
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
indentation:
level: error
spaces: 2
indent-sequences: true
check-multi-line-strings: false
line-length: disable
truthy: disable

View file

@ -6,77 +6,112 @@
# the integration's code owner is automatically notified.
# Core Code
setup.py @esphome/core
pyproject.toml @esphome/core
esphome/*.py @esphome/core
esphome/core/* @esphome/core
# Integrations
esphome/components/a01nyub/* @MrSuicideParrot
esphome/components/a02yyuw/* @TH-Braemer
esphome/components/absolute_humidity/* @DAVe3283
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu
esphome/components/ads1118/* @solomondg1
esphome/components/ags10/* @mak-42
esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/alarm_control_panel/* @grahambrown11
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier
esphome/components/am2315c/* @swoboda1337
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/am43/sensor/* @buxtronix
esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter
esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner
esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/audio/* @kahrendt
esphome/components/audio_dac/* @kbx81
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
esphome/components/bang_bang/* @OttoWinter
esphome/components/bedjet/* @jhansche
esphome/components/bedjet/climate/* @jhansche
esphome/components/bedjet/fan/* @jhansche
esphome/components/bedjet/sensor/* @javawizard @jhansche
esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas
esphome/components/ble_client/* @buxtronix
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bme68x_bsec2/* @kbx81 @neffs
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
esphome/components/bmi160/* @flaviut
esphome/components/bmp3xx/* @martgras
esphome/components/bmp280_base/* @ademuri
esphome/components/bmp280_i2c/* @ademuri
esphome/components/bmp280_spi/* @ademuri
esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter
esphome/components/cover/* @esphome/core
esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/cst226/* @clydebarrow
esphome/components/cst816/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet
@ -88,10 +123,15 @@ esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz
esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull
esphome/components/ens160/* @vincentscode
esphome/components/emmeti/* @E440QF
esphome/components/ens160/* @latonita
esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz
esphome/components/esp32_ble_client/* @jesserockz
@ -101,28 +141,43 @@ esphome/components/esp32_bt_common/* @RoboMagus
esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
@ -130,31 +185,45 @@ esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter
esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @OttoWinter @esphome/core
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
esphome/components/honeywell_hih_i2c/* @Benichou34
esphome/components/honeywellabp/* @RubyBailey
esphome/components/honeywellabp2_i2c/* @jpfaff
esphome/components/host/* @esphome/core
esphome/components/host/* @clydebarrow @esphome/core
esphome/components/host/time/* @clydebarrow
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
esphome/components/improv_serial/* @esphome/core
esphome/components/ina226/* @Sergio303 @latonita
esphome/components/ina260/* @mreditor97
esphome/components/ina2xx_base/* @latonita
esphome/components/ina2xx_i2c/* @latonita
esphome/components/ina2xx_spi/* @latonita
esphome/components/inkbird_ibsth1_mini/* @fkirill
esphome/components/inkplate6/* @jesserockz
esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @OttoWinter
esphome/components/kalman_combinator/* @Cat-Ion
esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb
@ -169,8 +238,13 @@ esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @sjtrny
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber
esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell
esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger
esphome/components/max6956/* @looping40
@ -192,6 +266,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz
esphome/components/mics_4514/* @jesserockz
@ -215,15 +290,21 @@ esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
esphome/components/nau7802/* @cujomalainey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @senexcrenshaw
esphome/components/nextion/* @edwardtfn @senexcrenshaw
esphome/components/nextion/binary_sensor/* @senexcrenshaw
esphome/components/nextion/sensor/* @senexcrenshaw
esphome/components/nextion/switch/* @senexcrenshaw
esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
@ -238,6 +319,11 @@ esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
esphome/components/pn532_spi/* @OttoWinter @jesserockz
esphome/components/pn7150/* @jesserockz @kbx81
esphome/components/pn7150_i2c/* @jesserockz @kbx81
esphome/components/pn7160/* @jesserockz @kbx81
esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/power_supply/* @esphome/core
esphome/components/preferences/* @esphome/core
esphome/components/psram/* @esphome/core
@ -246,6 +332,7 @@ esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
esphome/components/qwiic_pir/* @kahrendt
esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3
@ -259,13 +346,16 @@ esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
esphome/components/sdl/* @clydebarrow
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/seeed_mr24hpc1/* @limengdu
esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
@ -277,6 +367,7 @@ esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht3xd/* @mrtoy-me
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sigma_delta_output/* @Cat-Ion
@ -290,7 +381,7 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
@ -307,22 +398,35 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
esphome/components/st7701s/* @clydebarrow
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/template/alarm_control_panel/* @grahambrown11
esphome/components/tem3200/* @bakerkj
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter
esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc
@ -332,7 +436,7 @@ esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka
esphome/components/toshiba/* @kbx81
esphome/components/touchscreen/* @jesserockz
esphome/components/touchscreen/* @jesserockz @nielsnl68
esphome/components/tsl2591/* @wjcarpenter
esphome/components/tt21100/* @kroimon
esphome/components/tuya/binary_sensor/* @jesserockz
@ -344,27 +448,48 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/valve/* @esphome/core
esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz
esphome/components/wake_on_lan/* @willwill2will54
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
esphome/components/wk2132_i2c/* @DrCoolZic
esphome/components/wk2132_spi/* @DrCoolZic
esphome/components/wk2168_i2c/* @DrCoolZic
esphome/components/wk2168_spi/* @DrCoolZic
esphome/components/wk2204_i2c/* @DrCoolZic
esphome/components/wk2204_spi/* @DrCoolZic
esphome/components/wk2212_i2c/* @DrCoolZic
esphome/components/wk2212_spi/* @DrCoolZic
esphome/components/wl_134/* @hobbypunk90
esphome/components/x9c/* @EtienneMD
esphome/components/xgzp68xx/* @gcormier
esphome/components/xiaomi_hhccjcy10/* @fariouche
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/* @nielsnl68 @numo68
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt

View file

@ -10,5 +10,3 @@ Things to note when contributing:
for more information.
- Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
which checks if your new feature compiles correctly.
- Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping
me after some time.

View file

@ -7,3 +7,5 @@
For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues).
For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues).
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View file

@ -32,30 +32,15 @@ RUN \
python3-setuptools=66.1.1-1 \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \
curl=7.88.1-10+deb12u4 \
openssh-client=1:9.2p1-2+deb12u1 \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u8 \
openssh-client=1:9.2p1-2+deb12u3 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
patch=2.7.6-7; \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
apt-get install -y --no-install-recommends \
build-essential=12.9 \
python3-dev=3.11.2-1+b1 \
zlib1g-dev=1:1.2.13.dfsg-1 \
libjpeg-dev=1:2.1.5-2 \
libfreetype-dev=2.12.1+dfsg-5 \
libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6 \
cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \
fi; \
rm -rf \
patch=2.7.6-7 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
@ -81,7 +66,8 @@ RUN \
fi; \
pip3 install \
--break-system-packages --no-cache-dir \
platformio==6.1.11 \
# Keep platformio version in sync with requirements.txt
platformio==6.1.16 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
@ -91,14 +77,55 @@ RUN \
# First install requirements to leverage caching when requirements don't change
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini /
RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
pip3 install \
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
&& /platformio_install_deps.py /platformio.ini --libraries
COPY requirements.txt requirements_optional.txt /
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
# Fail on any non-zero status
set -e
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
then
curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple";
fi
# install build tools in case wheels are not available
BUILD_DEPS="
build-essential=12.9
python3-dev=3.11.2-1+b1
zlib1g-dev=1:1.2.13.dfsg-1
libjpeg-dev=1:2.1.5-2
libfreetype-dev=2.12.1+dfsg-5+deb12u3
libssl-dev=3.0.15-1~deb12u1
libffi-dev=3.4.4-1
libopenjp2-7=2.5.0-2
libtiff6=4.5.0-6+deb12u1
cargo=0.66.0+ds1-1
pkg-config=1.8.1-1
"
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
then
apt-get update
apt-get install -y --no-install-recommends $BUILD_DEPS
fi
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
then
apt-get remove -y --purge --auto-remove $BUILD_DEPS
rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
fi
END-OF-RUN
COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries
# Avoid unsafe git error when container user and file config volume permissions don't match
RUN git config --system --add safe.directory '*'
# ======================= docker-type image =======================
@ -110,7 +137,7 @@ RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
--break-system-packages --no-cache-dir -e /esphome
# Settings for dashboard
ENV USERNAME="" PASSWORD=""
@ -160,7 +187,7 @@ RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
--break-system-packages --no-cache-dir -e /esphome
# Labels
LABEL \
@ -186,8 +213,8 @@ RUN \
clang-format-13=1:13.0.1-11+b2 \
clang-tidy-14=1:14.0.6-12 \
patch=2.7.6-7 \
software-properties-common=0.99.30-4 \
nano=7.2-1 \
software-properties-common=0.99.30-4.1~deb12u1 \
nano=7.2-1+deb12u1 \
build-essential=12.9 \
python3-dev=3.11.2-1+b1 \
&& rm -rf \

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# If /cache is mounted, use that as PIO's coredir
# otherwise use path in /config (so that PIO packages aren't downloaded on each compile)
@ -21,4 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# If /build is mounted, use that as the build path
# otherwise use path in /config (so that builds aren't lost on container restart)
if [[ -d /build ]]; then
export ESPHOME_BUILD_PATH=/build
fi
exec esphome "$@"

View file

@ -0,0 +1,47 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# This file installs the user ESPHome fork if specified.
# The fork must be up to date with the latest ESPHome dev branch
# and have no conflicts.
# This config option only exists in the ESPHome Dev add-on.
# ==============================================================================
declare esphome_fork
if bashio::config.has_value 'esphome_fork'; then
esphome_fork=$(bashio::config 'esphome_fork')
# format: [username][/repository]:ref
if [[ "$esphome_fork" =~ ^(([^/]+)(/([^:]+))?:)?([^:/]+)$ ]]; then
username="${BASH_REMATCH[2]:-esphome}"
repository="${BASH_REMATCH[4]:-esphome}"
ref="${BASH_REMATCH[5]}"
else
bashio::exit.nok "Invalid esphome_fork format: $esphome_fork"
fi
full_url="https://github.com/${username}/${repository}/archive/${ref}.tar.gz"
bashio::log.info "Checking forked ESPHome"
dev_version=$(python3 -c "from esphome.const import __version__; print(__version__)")
bashio::log.info "Downloading ESPHome from fork '${esphome_fork}' (${full_url})..."
curl -L -o /tmp/esphome.tar.gz "${full_url}" -qq ||
bashio::exit.nok "Failed downloading ESPHome fork."
bashio::log.info "Installing ESPHome from fork '${esphome_fork}' (${full_url})..."
rm -rf /esphome || bashio::exit.nok "Failed to remove ESPHome."
mkdir /esphome
tar -zxf /tmp/esphome.tar.gz -C /esphome --strip-components=1 ||
bashio::exit.nok "Failed installing ESPHome from fork."
pip install -U -e /esphome || bashio::exit.nok "Failed installing ESPHome from fork."
rm -f /tmp/esphome.tar.gz
fork_version=$(python3 -c "from esphome.const import __version__; print(__version__)")
if [[ "$fork_version" != "$dev_version" ]]; then
bashio::log.error "############################"
bashio::log.error "Uninstalled fork as version does not match"
bashio::log.error "Update (or ask the author to update) the branch"
bashio::log.error "This is important as the dev addon and the dev ESPHome"
bashio::log.error "branch can have changes that are not compatible with old forks"
bashio::log.error "and get reported as bugs which we cannot solve easily."
bashio::log.error "############################"
bashio::exit.nok
fi
bashio::log.info "Installed ESPHome from fork '${esphome_fork}' (${full_url})..."
fi

View file

@ -1,51 +1,55 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from datetime import datetime
import functools
import logging
import os
import re
import sys
import time
from datetime import datetime
import argcomplete
from esphome import const, writer, yaml_util
import esphome.codegen as cg
from esphome.config import iter_components, read_config, strip_default_ids
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
CONF_BAUD_RATE,
CONF_BROKER,
CONF_DEASSERT_RTS_DTR,
CONF_DISABLED,
CONF_ESPHOME,
CONF_LEVEL,
CONF_LOG_TOPIC,
CONF_LOGGER,
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_OTA,
CONF_MQTT,
CONF_MDNS,
CONF_DISABLED,
CONF_PASSWORD,
CONF_PORT,
CONF_ESPHOME,
CONF_PLATFORM,
CONF_PLATFORMIO_OPTIONS,
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_TOPIC,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
SECRETS_FILES,
)
from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import indent, is_ip_address
from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import Fore, color, setup_log
from esphome.util import (
get_serial_ports,
list_yaml_files,
run_external_command,
run_external_process,
safe_print,
list_yaml_files,
get_serial_ports,
)
from esphome.log import color, setup_log, Fore
_LOGGER = logging.getLogger(__name__)
@ -65,7 +69,7 @@ def choose_prompt(options, purpose: str = None):
f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
)
for i, (desc, _) in enumerate(options):
safe_print(f" [{i+1}] {desc}")
safe_print(f" [{i + 1}] {desc}")
while True:
opt = input("(number): ")
@ -94,8 +98,12 @@ def choose_upload_log_host(
options.append((f"Over The Air ({CORE.address})", CORE.address))
if default == "OTA":
return CORE.address
if show_mqtt and CONF_MQTT in CORE.config:
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
if (
show_mqtt
and (mqtt_config := CORE.config.get(CONF_MQTT))
and mqtt_logging_enabled(mqtt_config)
):
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if default == "OTA":
return "MQTT"
if default is not None:
@ -105,6 +113,17 @@ def choose_upload_log_host(
return choose_prompt(options, purpose=purpose)
def mqtt_logging_enabled(mqtt_config):
log_topic = mqtt_config[CONF_LOG_TOPIC]
if log_topic is None:
return False
if CONF_TOPIC not in log_topic:
return False
if log_topic.get(CONF_LEVEL, None) == "NONE":
return False
return True
def get_port_type(port):
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
@ -115,6 +134,7 @@ def get_port_type(port):
def run_miniterm(config, port):
import serial
from esphome import platformio_api
if CONF_LOGGER not in config:
@ -196,7 +216,7 @@ def write_cpp(config):
def generate_cpp_contents(config):
_LOGGER.info("Generating C++ source...")
for name, component, conf in iter_components(CORE.config):
for name, component, conf in iter_component_configs(CORE.config):
if component.to_code is not None:
coro = wrap_to_code(name, component)
CORE.add_job(coro, conf)
@ -297,8 +317,27 @@ def upload_using_platformio(config, port):
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def check_permissions(port):
if os.name == "posix" and get_port_type(port) == "SERIAL":
# Check if we can open selected serial port
if not os.access(port, os.F_OK):
raise EsphomeError(
"The selected serial port does not exist. To resolve this issue, "
"check that the device is connected to this computer with a USB cable and that "
"the USB cable can be used for data and is not a power-only cable."
)
if not (os.access(port, os.R_OK | os.W_OK)):
raise EsphomeError(
"You do not have read or write permission on the selected serial port. "
"To resolve this issue, you can add your user to the dialout group "
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. "
"You will need to log out & back in or reboot to activate the new group access."
)
def upload_program(config, args, host):
if get_port_type(host) == "SERIAL":
check_permissions(host)
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
file = getattr(args, "file", None)
return upload_using_esptool(config, host, file)
@ -311,22 +350,27 @@ def upload_program(config, args, host):
return 1 # Unknown target platform
if CONF_OTA not in config:
ota_conf = {}
for ota_item in config.get(CONF_OTA, []):
if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
ota_conf = ota_item
break
if not ota_conf:
raise EsphomeError(
"Cannot upload Over the Air as the config does not include the ota: "
"component"
f"Cannot upload Over the Air as the {CONF_OTA} configuration is not present or does not include {CONF_PLATFORM}: {CONF_ESPHOME}"
)
from esphome import espota2
ota_conf = config[CONF_OTA]
remote_port = ota_conf[CONF_PORT]
password = ota_conf.get(CONF_PASSWORD, "")
if (
not is_ip_address(CORE.address)
not is_ip_address(CORE.address) # pylint: disable=too-many-boolean-expressions
and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
and CONF_MQTT in config
and (not args.device or args.device in ("MQTT", "OTA"))
):
from esphome import mqtt
@ -344,6 +388,7 @@ def show_logs(config, args, port):
if "logger" not in config:
raise EsphomeError("Logger is not configured!")
if get_port_type(port) == "SERIAL":
check_permissions(port)
return run_miniterm(config, port)
if get_port_type(port) == "NETWORK" and "api" in config:
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
@ -351,7 +396,7 @@ def show_logs(config, args, port):
port = mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id
)
)[0]
from esphome.components.api.client import run_logs
@ -462,6 +507,15 @@ def command_run(args, config):
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
from esphome.platformio_api import get_idedata
idedata = get_idedata(config)
if idedata is None:
return 1
program_path = idedata.raw["prog_path"]
return run_external_process(program_path)
port = choose_upload_log_host(
default=args.device,
check_default=None,
@ -561,9 +615,10 @@ def command_update_all(args):
def command_idedata(args, config):
from esphome import platformio_api
import json
from esphome import platformio_api
logging.disable(logging.INFO)
logging.disable(logging.WARNING)
@ -660,6 +715,7 @@ def command_rename(args, config):
os.remove(new_path)
return 1
if CORE.config_path != new_path:
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
@ -693,7 +749,11 @@ POST_CONFIG_ACTIONS = {
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
"-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true"
"-v",
"--verbose",
help="Enable verbose ESPHome logs.",
action="store_true",
default=get_bool_env("ESPHOME_VERBOSE"),
)
options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
@ -711,7 +771,14 @@ def parse_args(argv):
)
parser = argparse.ArgumentParser(
description=f"ESPHome v{const.__version__}", parents=[options_parser]
description=f"ESPHome {const.__version__}", parents=[options_parser]
)
parser.add_argument(
"--version",
action="version",
version=f"Version: {const.__version__}",
help="Print the ESPHome version and exit.",
)
mqtt_options = argparse.ArgumentParser(add_help=False)
@ -748,7 +815,9 @@ def parse_args(argv):
)
parser_upload = subparsers.add_parser(
"upload", help="Validate the configuration and upload the latest binary."
"upload",
help="Validate the configuration and upload the latest binary.",
parents=[mqtt_options],
)
parser_upload.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
@ -765,6 +834,7 @@ def parse_args(argv):
parser_logs = subparsers.add_parser(
"logs",
help="Validate the configuration and show all logs.",
aliases=["log"],
parents=[mqtt_options],
)
parser_logs.add_argument(
@ -909,67 +979,6 @@ def parse_args(argv):
# a deprecation warning).
arguments = argv[1:]
# On Python 3.9+ we can simply set exit_on_error=False in the constructor
def _raise(x):
raise argparse.ArgumentError(None, x)
# First, try new-style parsing, but don't exit in case of failure
try:
# duplicate parser so that we can use the original one to raise errors later on
current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
current_parser.set_defaults(deprecated_argv_suggestion=None)
current_parser.error = _raise
return current_parser.parse_args(arguments)
except argparse.ArgumentError:
pass
# Second, try compat parsing and rearrange the command-line if it succeeds
# Disable argparse's built-in help option and add it manually to prevent this
# parser from printing the help messagefor the old format when invoked with -h.
compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
compat_parser.add_argument("-h", "--help", action="store_true")
compat_parser.add_argument("configuration", nargs="*")
compat_parser.add_argument(
"command",
choices=[
"config",
"compile",
"upload",
"logs",
"run",
"clean-mqtt",
"wizard",
"mqtt-fingerprint",
"version",
"clean",
"dashboard",
"vscode",
"update-all",
],
)
try:
compat_parser.error = _raise
result, unparsed = compat_parser.parse_known_args(argv[1:])
last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
unparsed = [
"--device" if arg in ("--upload-port", "--serial-port") else arg
for arg in unparsed
]
arguments = (
arguments[0:last_option]
+ [result.command]
+ result.configuration
+ unparsed
)
deprecated_argv_suggestion = arguments
except argparse.ArgumentError:
# old-style parsing failed, don't suggest any argument
deprecated_argv_suggestion = None
# Finally, run the new-style parser again with the possibly swapped arguments,
# and let it error out if the command is unparsable.
parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
argcomplete.autocomplete(parser)
return parser.parse_args(arguments)
@ -984,20 +993,6 @@ def run_esphome(argv):
# Show timestamp for dashboard access logs
args.command == "dashboard",
)
if args.deprecated_argv_suggestion is not None and args.command != "vscode":
_LOGGER.warning(
"Calling ESPHome with the configuration before the command is deprecated "
"and will be removed in the future. "
)
_LOGGER.warning("Please instead use:")
_LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion))
if sys.version_info < (3, 8, 0):
_LOGGER.error(
"You're running ESPHome with Python <3.8. ESPHome is no longer compatible "
"with this Python version. Please reinstall ESPHome with Python 3.8+"
)
return 1
if args.command in PRE_CONFIG_ACTIONS:
try:

View file

@ -1,16 +1,18 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ALL,
CONF_ANY,
CONF_AUTOMATION_ID,
CONF_CONDITION,
CONF_COUNT,
CONF_ELSE,
CONF_ID,
CONF_THEN,
CONF_TIME,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TYPE_ID,
CONF_TIME,
CONF_UPDATE_INTERVAL,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -18,10 +20,20 @@ from esphome.util import Registry
def maybe_simple_id(*validators):
"""Allow a raw ID to be specified in place of a config block.
If the value that's being validated is a dictionary, it's passed as-is to the specified validators. Otherwise, it's
wrapped in a dict that looks like ``{"id": <value>}``, and that dict is then handed off to the specified validators.
"""
return maybe_conf(CONF_ID, *validators)
def maybe_conf(conf, *validators):
"""Allow a raw value to be specified in place of a config block.
If the value that's being validated is a dictionary, it's passed as-is to the specified validators. Otherwise, it's
wrapped in a dict that looks like ``{<conf>: <value>}``, and that dict is then handed off to the specified
validators.
(This is a general case of ``maybe_simple_id`` that allows the wrapping key to be something other than ``id``.)
"""
validator = cv.All(*validators)
@schema_extractor("maybe")
@ -63,6 +75,13 @@ def validate_potentially_and_condition(value):
return validate_condition(value)
def validate_potentially_or_condition(value):
if isinstance(value, list):
with cv.remove_prepend_path(["or"]):
return validate_condition({"or": value})
return validate_condition(value)
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
@ -156,6 +175,18 @@ async def or_condition_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("all", AndCondition, validate_condition_list)
async def all_condition_to_code(config, condition_id, template_arg, args):
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("any", OrCondition, validate_condition_list)
async def any_condition_to_code(config, condition_id, template_arg, args):
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("not", NotCondition, validate_potentially_and_condition)
async def not_condition_to_code(config, condition_id, template_arg, args):
condition = await build_condition(config, template_arg, args)
@ -213,15 +244,21 @@ async def delay_action_to_code(config, action_id, template_arg, args):
IfAction,
cv.All(
{
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
cv.Exclusive(
CONF_CONDITION, CONF_CONDITION
): validate_potentially_and_condition,
cv.Exclusive(CONF_ANY, CONF_CONDITION): validate_potentially_or_condition,
cv.Exclusive(CONF_ALL, CONF_CONDITION): validate_potentially_and_condition,
cv.Optional(CONF_THEN): validate_action_list,
cv.Optional(CONF_ELSE): validate_action_list,
},
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
)
async def if_action_to_code(config, action_id, template_arg, args):
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
conditions = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, conditions)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)

View file

@ -8,83 +8,86 @@
# want to break suddenly due to a rename (this file will get backports for features).
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
LineComment,
MockObj,
MockObjClass,
Pvariable,
RawExpression,
RawStatement,
TemplateArguments,
StructInitializer,
ArrayInitializer,
safe_exp,
Statement,
LineComment,
progmem_array,
static_const_array,
statement,
variable,
with_local_variable,
new_variable,
Pvariable,
new_Pvariable,
StructInitializer,
TemplateArguments,
add,
add_global,
add_library,
add_build_flag,
add_define,
add_global,
add_library,
add_platformio_option,
get_variable,
get_variable_with_full_id,
process_lambda,
is_template,
new_Pvariable,
new_variable,
process_lambda,
progmem_array,
safe_exp,
statement,
static_const_array,
templatable,
MockObj,
MockObjClass,
variable,
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa
gpio_pin_expression,
register_component,
from esphome.cpp_helpers import ( # noqa: F401
build_registry_entry,
build_registry_list,
extract_registry_entry_config,
register_parented,
gpio_pin_expression,
past_safe_mode,
register_component,
register_parented,
)
from esphome.cpp_types import ( # noqa
global_ns,
void,
nullptr,
float_,
double,
from esphome.cpp_types import ( # noqa: F401
NAN,
App,
Application,
Component,
ComponentPtr,
Controller,
EntityBase,
EntityCategory,
ESPTime,
GPIOPin,
InternalGPIOPin,
JsonObject,
JsonObjectConst,
Parented,
PollingComponent,
arduino_json_ns,
bool_,
const_char_ptr,
double,
esphome_ns,
float_,
global_ns,
gpio_Flags,
int16,
int32,
int64,
int_,
nullptr,
optional,
size_t,
std_ns,
std_shared_ptr,
std_string,
std_string_ref,
std_vector,
uint8,
uint16,
uint32,
uint64,
int16,
int32,
int64,
size_t,
const_char_ptr,
NAN,
esphome_ns,
App,
EntityBase,
Component,
ComponentPtr,
PollingComponent,
Application,
optional,
arduino_json_ns,
JsonObject,
JsonObjectConst,
Controller,
GPIOPin,
InternalGPIOPin,
gpio_Flags,
EntityCategory,
Parented,
void,
)

View file

@ -0,0 +1 @@
CODEOWNERS = ["@TH-Braemer"]

View file

@ -0,0 +1,43 @@
// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311
#include "a02yyuw.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace a02yyuw {
static const char *const TAG = "a02yyuw.sensor";
void A02yyuwComponent::loop() {
uint8_t data;
while (this->available() > 0) {
this->read_byte(&data);
if (this->buffer_.empty() && (data != 0xff))
continue;
buffer_.push_back(data);
if (this->buffer_.size() == 4)
this->check_buffer_();
}
}
void A02yyuwComponent::check_buffer_() {
uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2];
if (this->buffer_[3] == checksum) {
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
if (distance > 30) {
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
this->publish_state(distance);
} else {
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
}
this->buffer_.clear();
}
void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); }
} // namespace a02yyuw
} // 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 a02yyuw {
class A02yyuwComponent : 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 a02yyuw
} // 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,
ICON_ARROW_EXPAND_VERTICAL,
DEVICE_CLASS_DISTANCE,
UNIT_MILLIMETER,
)
CODEOWNERS = ["@TH-Braemer"]
DEPENDENCIES = ["uart"]
a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw")
A02yyuwComponent = a02yyuw_ns.class_(
"A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice
)
CONFIG_SCHEMA = sensor.sensor_schema(
A02yyuwComponent,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_ARROW_EXPAND_VERTICAL,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_DISTANCE,
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"a02yyuw",
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

@ -18,11 +18,23 @@ from esphome.components.esp32.const import (
CODEOWNERS = ["@esphome/core"]
adc_ns = cg.esphome_ns.namespace("adc")
"""
From the below patch versions (and 5.2+) ADC_ATTEN_DB_11 is deprecated and replaced with ADC_ATTEN_DB_12.
4.4.7
5.0.5
5.1.3
5.2+
"""
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,
"11db": adc_ns.ADC_ATTEN_DB_12_COMPAT,
"12db": adc_ns.ADC_ATTEN_DB_12_COMPAT,
"auto": "auto",
}
@ -139,6 +151,9 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
VARIANT_ESP32C2: {},
VARIANT_ESP32C6: {},
VARIANT_ESP32H2: {},
}

View file

@ -46,27 +46,27 @@ extern "C"
ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
#if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040)
pin_->setup();
this->pin_->setup();
#endif
#ifdef USE_ESP32
if (channel1_ != ADC1_CHANNEL_MAX) {
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
if (!autorange_) {
adc1_config_channel_atten(channel1_, attenuation_);
if (!this->autorange_) {
adc1_config_channel_atten(this->channel1_, this->attenuation_);
}
} else if (channel2_ != ADC2_CHANNEL_MAX) {
if (!autorange_) {
adc2_config_channel_atten(channel2_, attenuation_);
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
if (!this->autorange_) {
adc2_config_channel_atten(this->channel2_, this->attenuation_);
}
}
// load characteristics for each attenuation
for (int32_t i = 0; i <= ADC_ATTEN_DB_11; i++) {
auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
auto adc_unit = this->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
&cal_characteristics_[i]);
&this->cal_characteristics_[i]);
switch (cal_value) {
case ESP_ADC_CAL_VAL_EFUSE_VREF:
ESP_LOGV(TAG, "Using eFuse Vref for calibration");
@ -99,13 +99,13 @@ void ADCSensor::dump_config() {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", pin_);
LOG_PIN(" Pin: ", this->pin_);
#endif
#endif // USE_ESP8266 || USE_LIBRETINY
#ifdef USE_ESP32
LOG_PIN(" Pin: ", pin_);
if (autorange_) {
LOG_PIN(" Pin: ", this->pin_);
if (this->autorange_) {
ESP_LOGCONFIG(TAG, " Attenuation: auto");
} else {
switch (this->attenuation_) {
@ -118,8 +118,8 @@ void ADCSensor::dump_config() {
case ADC_ATTEN_DB_6:
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
break;
case ADC_ATTEN_DB_11:
ESP_LOGCONFIG(TAG, " Attenuation: 11db");
case ADC_ATTEN_DB_12_COMPAT:
ESP_LOGCONFIG(TAG, " Attenuation: 12db");
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
@ -134,11 +134,11 @@ void ADCSensor::dump_config() {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", pin_);
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
}
#endif // USE_RP2040
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
LOG_UPDATE_INTERVAL(this);
}
@ -149,14 +149,24 @@ void ADCSensor::update() {
this->publish_state(value_v);
}
void ADCSensor::set_sample_count(uint8_t sample_count) {
if (sample_count != 0) {
this->sample_count_ = sample_count;
}
}
#ifdef USE_ESP8266
float ADCSensor::sample() {
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
#ifdef USE_ADC_SENSOR_VCC
int32_t raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else
int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT
raw += analogRead(this->pin_->get_pin()); // NOLINT
#endif
if (output_raw_) {
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return raw;
}
return raw / 1024.0f;
@ -165,77 +175,81 @@ float ADCSensor::sample() {
#ifdef USE_ESP32
float ADCSensor::sample() {
if (!autorange_) {
if (!this->autorange_) {
uint32_t sum = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
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 (this->channel1_ != ADC1_CHANNEL_MAX) {
raw = adc1_get_raw(this->channel1_);
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
}
if (raw == -1) {
return NAN;
}
if (output_raw_) {
return raw;
sum += raw;
}
uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]);
sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return sum;
}
uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]);
return mv / 1000.0f;
}
int raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
if (channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11);
raw11 = adc1_get_raw(channel1_);
if (raw11 < ADC_MAX) {
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6);
raw6 = adc1_get_raw(channel1_);
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT);
raw12 = adc1_get_raw(this->channel1_);
if (raw12 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6);
raw6 = adc1_get_raw(this->channel1_);
if (raw6 < ADC_MAX) {
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5);
raw2 = adc1_get_raw(channel1_);
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5);
raw2 = adc1_get_raw(this->channel1_);
if (raw2 < ADC_MAX) {
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(channel1_);
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(this->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);
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12);
if (raw12 < ADC_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6);
adc2_get_raw(this->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);
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5);
adc2_get_raw(this->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);
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0);
}
}
}
}
if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw11 == -1) {
if (raw0 == -1 || raw2 == -1 || raw6 == -1 || raw12 == -1) {
return NAN;
}
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_[(int32_t) ADC_ATTEN_DB_6]);
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_[(int32_t) ADC_ATTEN_DB_0]);
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]);
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->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)
uint32_t c11 = std::min(raw11, ADC_HALF);
uint32_t c12 = std::min(raw12, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
// max theoretical csum value is 4096*4 = 16384
uint32_t csum = c11 + c6 + c2 + c0;
uint32_t csum = c12 + c6 + c2 + c0;
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
return mv_scaled / (float) (csum * 1000U);
}
#endif // USE_ESP32
@ -246,8 +260,11 @@ float ADCSensor::sample() {
adc_set_temp_sensor_enabled(true);
delay(1);
adc_select_input(4);
int32_t raw = adc_read();
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
adc_set_temp_sensor_enabled(false);
if (this->output_raw_) {
return raw;
@ -268,7 +285,11 @@ float ADCSensor::sample() {
adc_gpio_init(pin);
adc_select_input(pin - 26);
int32_t raw = adc_read();
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
@ -276,7 +297,7 @@ float ADCSensor::sample() {
}
#endif // CYW43_USES_VSYS_PIN
if (output_raw_) {
if (this->output_raw_) {
return raw;
}
float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0;
@ -287,10 +308,19 @@ float ADCSensor::sample() {
#ifdef USE_LIBRETINY
float ADCSensor::sample() {
if (output_raw_) {
return analogRead(this->pin_->get_pin()); // NOLINT
uint32_t raw = 0;
if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogRead(this->pin_->get_pin()); // NOLINT
}
return analogReadVoltage(this->pin_->get_pin()) / 1000.0f; // NOLINT
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw;
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw / 1000.0f;
}
#endif // USE_LIBRETINY

View file

@ -1,33 +1,48 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/defines.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP32
#include "driver/adc.h"
#include <esp_adc_cal.h>
#include "driver/adc.h"
#endif
namespace esphome {
namespace adc {
#ifdef USE_ESP32
// clang-format off
#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \
(ESP_IDF_VERSION_MAJOR == 5 && \
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
(ESP_IDF_VERSION_MINOR >= 2)) \
)
// clang-format on
static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_12;
#else
static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
#endif
#endif // USE_ESP32
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
#ifdef USE_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) { this->attenuation_ = attenuation; }
void set_channel1(adc1_channel_t channel) {
channel1_ = channel;
channel2_ = ADC2_CHANNEL_MAX;
this->channel1_ = channel;
this->channel2_ = ADC2_CHANNEL_MAX;
}
void set_channel2(adc2_channel_t channel) {
channel2_ = channel;
channel1_ = ADC1_CHANNEL_MAX;
this->channel2_ = channel;
this->channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { autorange_ = autorange; }
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif
/// Update ADC values
@ -38,7 +53,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
/// `HARDWARE_LATE` setup priority
float get_setup_priority() const override;
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) { this->output_raw_ = output_raw; }
void set_sample_count(uint8_t sample_count);
float sample() override;
#ifdef USE_ESP8266
@ -46,12 +62,13 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#endif
#ifdef USE_RP2040
void set_is_temperature() { is_temperature_ = true; }
void set_is_temperature() { this->is_temperature_ = true; }
#endif
protected:
InternalGPIOPin *pin_;
bool output_raw_{false};
uint8_t sample_count_{1};
#ifdef USE_RP2040
bool is_temperature_{false};

View file

@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
@ -19,16 +21,35 @@ from . import (
ATTENUATION_MODES,
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
adc_ns,
validate_adc_pin,
)
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["voltage_sampler"]
CONF_SAMPLES = "samples"
_attenuation = cv.enum(ATTENUATION_MODES, lower=True)
def validate_config(config):
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set"
)
if config.get(CONF_ATTENUATION) == "11db":
_LOGGER.warning(
"`attenuation: 11db` is deprecated, use `attenuation: 12db` instead"
)
# Alter value here so `config` command prints the recommended change
config[CONF_ATTENUATION] = _attenuation("12db")
return config
@ -47,7 +68,6 @@ def final_validate_config(config):
return config
adc_ns = cg.esphome_ns.namespace("adc")
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@ -65,8 +85,9 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): validate_adc_pin,
cv.Optional(CONF_RAW, default=False): cv.boolean,
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True)
cv.only_on_esp32, _attenuation
),
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
}
)
.extend(cv.polling_component_schema("60s")),
@ -90,6 +111,7 @@ async def to_code(config):
cg.add(var.set_pin(pin))
cg.add(var.set_output_raw(config[CONF_RAW]))
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":

View file

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

View file

@ -0,0 +1,304 @@
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "ade7880.h"
#include "ade7880_registers.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace ade7880 {
static const char *const TAG = "ade7880";
void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; }
void ADE7880::setup() {
if (this->irq0_pin_ != nullptr) {
this->irq0_pin_->setup();
}
this->irq1_pin_->setup();
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
this->store_.irq1_pin = this->irq1_pin_->to_isr();
this->irq1_pin_->attach_interrupt(ADE7880Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE);
// if IRQ1 is already asserted, the cause must be determined
if (this->irq1_pin_->digital_read() == 0) {
ESP_LOGD(TAG, "IRQ1 found asserted during setup()");
auto status1 = read_u32_register16_(STATUS1);
if ((status1 & ~STATUS1_RSTDONE) != 0) {
// not safe to proceed, must initiate reset
ESP_LOGD(TAG, "IRQ1 asserted for !RSTDONE, resetting device");
this->reset_device_();
return;
}
if ((status1 & STATUS1_RSTDONE) == STATUS1_RSTDONE) {
// safe to proceed, device has just completed reset cycle
ESP_LOGD(TAG, "Acknowledging RSTDONE");
this->write_u32_register16_(STATUS0, 0xFFFF);
this->write_u32_register16_(STATUS1, 0xFFFF);
this->init_device_();
return;
}
}
this->reset_device_();
}
void ADE7880::loop() {
// check for completion of a reset cycle
if (!this->store_.reset_done) {
return;
}
ESP_LOGD(TAG, "Acknowledging RSTDONE");
this->write_u32_register16_(STATUS0, 0xFFFF);
this->write_u32_register16_(STATUS1, 0xFFFF);
this->init_device_();
this->store_.reset_done = false;
this->store_.reset_pending = false;
}
template<typename F>
void ADE7880::update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s24zp_register16_(a_register);
sensor->publish_state(f(val));
}
template<typename F>
void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s16_register16_(a_register);
sensor->publish_state(f(val));
}
template<typename F>
void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
if (sensor == nullptr) {
return;
}
float val = this->read_s32_register16_(a_register);
sensor->publish_state(f(val));
}
void ADE7880::update() {
if (this->store_.reset_pending) {
return;
}
auto start = millis();
if (this->channel_n_ != nullptr) {
auto *chan = this->channel_n_;
this->update_sensor_from_s24zp_register16_(chan->current, NIRMS, [](float val) { return val / 100000.0f; });
}
if (this->channel_a_ != nullptr) {
auto *chan = this->channel_a_;
this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
if (this->channel_b_ != nullptr) {
auto *chan = this->channel_b_;
this->update_sensor_from_s24zp_register16_(chan->current, BIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, BWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
if (this->channel_c_ != nullptr) {
auto *chan = this->channel_c_;
this->update_sensor_from_s24zp_register16_(chan->current, CIRMS, [](float val) { return val / 100000.0f; });
this->update_sensor_from_s24zp_register16_(chan->voltage, CVRMS, [](float val) { return val / 10000.0f; });
this->update_sensor_from_s24zp_register16_(chan->active_power, CWATT, [](float val) { return val / 100.0f; });
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
[](float val) { return std::abs(val / -327.68f); });
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);
}
void ADE7880::dump_config() {
ESP_LOGCONFIG(TAG, "ADE7880:");
LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_);
LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_);
LOG_PIN(" RESET Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_);
if (this->channel_a_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase A:");
LOG_SENSOR(" ", "Current", this->channel_a_->current);
LOG_SENSOR(" ", "Voltage", this->channel_a_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_a_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_a_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_a_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_a_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_a_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_a_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_a_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_a_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_a_->phase_angle_calibration);
}
if (this->channel_b_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase B:");
LOG_SENSOR(" ", "Current", this->channel_b_->current);
LOG_SENSOR(" ", "Voltage", this->channel_b_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_b_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_b_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_b_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_b_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_b_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_b_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_b_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_b_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_b_->phase_angle_calibration);
}
if (this->channel_c_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase C:");
LOG_SENSOR(" ", "Current", this->channel_c_->current);
LOG_SENSOR(" ", "Voltage", this->channel_c_->voltage);
LOG_SENSOR(" ", "Active Power", this->channel_c_->active_power);
LOG_SENSOR(" ", "Apparent Power", this->channel_c_->apparent_power);
LOG_SENSOR(" ", "Power Factor", this->channel_c_->power_factor);
LOG_SENSOR(" ", "Forward Active Energy", this->channel_c_->forward_active_energy);
LOG_SENSOR(" ", "Reverse Active Energy", this->channel_c_->reverse_active_energy);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_c_->current_gain_calibration);
ESP_LOGCONFIG(TAG, " Voltage: %" PRId32, this->channel_c_->voltage_gain_calibration);
ESP_LOGCONFIG(TAG, " Power: %" PRId32, this->channel_c_->power_gain_calibration);
ESP_LOGCONFIG(TAG, " Phase Angle: %u", this->channel_c_->phase_angle_calibration);
}
if (this->channel_n_ != nullptr) {
ESP_LOGCONFIG(TAG, " Neutral:");
LOG_SENSOR(" ", "Current", this->channel_n_->current);
ESP_LOGCONFIG(TAG, " Calibration:");
ESP_LOGCONFIG(TAG, " Current: %" PRId32, this->channel_n_->current_gain_calibration);
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
void ADE7880::calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration) {
if (calibration == 0) {
return;
}
this->write_s10zp_register16_(a_register, calibration);
}
void ADE7880::calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration) {
if (calibration == 0) {
return;
}
this->write_s24zpse_register16_(a_register, calibration);
}
void ADE7880::init_device_() {
this->write_u8_register16_(CONFIG2, CONFIG2_I2C_LOCK);
this->write_u16_register16_(GAIN, 0);
if (this->frequency_ > 55) {
this->write_u16_register16_(COMPMODE, COMPMODE_DEFAULT | COMPMODE_SELFREQ);
}
if (this->channel_n_ != nullptr) {
this->calibrate_s24zpse_reading_(NIGAIN, this->channel_n_->current_gain_calibration);
}
if (this->channel_a_ != nullptr) {
this->calibrate_s24zpse_reading_(AIGAIN, this->channel_a_->current_gain_calibration);
this->calibrate_s24zpse_reading_(AVGAIN, this->channel_a_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(APGAIN, this->channel_a_->power_gain_calibration);
this->calibrate_s10zp_reading_(APHCAL, this->channel_a_->phase_angle_calibration);
}
if (this->channel_b_ != nullptr) {
this->calibrate_s24zpse_reading_(BIGAIN, this->channel_b_->current_gain_calibration);
this->calibrate_s24zpse_reading_(BVGAIN, this->channel_b_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(BPGAIN, this->channel_b_->power_gain_calibration);
this->calibrate_s10zp_reading_(BPHCAL, this->channel_b_->phase_angle_calibration);
}
if (this->channel_c_ != nullptr) {
this->calibrate_s24zpse_reading_(CIGAIN, this->channel_c_->current_gain_calibration);
this->calibrate_s24zpse_reading_(CVGAIN, this->channel_c_->voltage_gain_calibration);
this->calibrate_s24zpse_reading_(CPGAIN, this->channel_c_->power_gain_calibration);
this->calibrate_s10zp_reading_(CPHCAL, this->channel_c_->phase_angle_calibration);
}
// write three default values to data memory RAM to flush the I2C write queue
this->write_s32_register16_(VLEVEL, 0);
this->write_s32_register16_(VLEVEL, 0);
this->write_s32_register16_(VLEVEL, 0);
this->write_u8_register16_(DSPWP_SEL, DSPWP_SEL_SET);
this->write_u8_register16_(DSPWP_SET, DSPWP_SET_RO);
this->write_u16_register16_(RUN, RUN_ENABLE);
}
void ADE7880::reset_device_() {
if (this->reset_pin_ != nullptr) {
ESP_LOGD(TAG, "Reset device using RESET pin");
this->reset_pin_->digital_write(false);
delay(1);
this->reset_pin_->digital_write(true);
} else {
ESP_LOGD(TAG, "Reset device using SWRST command");
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
}
this->store_.reset_pending = true;
}
} // namespace ade7880
} // namespace esphome

View file

@ -0,0 +1,131 @@
#pragma once
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "ade7880_registers.h"
namespace esphome {
namespace ade7880 {
struct NeutralChannel {
void set_current(sensor::Sensor *sens) { this->current = sens; }
void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; }
sensor::Sensor *current{nullptr};
int32_t current_gain_calibration{0};
};
struct PowerChannel {
void set_current(sensor::Sensor *sens) { this->current = sens; }
void set_voltage(sensor::Sensor *sens) { this->voltage = sens; }
void set_active_power(sensor::Sensor *sens) { this->active_power = sens; }
void set_apparent_power(sensor::Sensor *sens) { this->apparent_power = sens; }
void set_power_factor(sensor::Sensor *sens) { this->power_factor = sens; }
void set_forward_active_energy(sensor::Sensor *sens) { this->forward_active_energy = sens; }
void set_reverse_active_energy(sensor::Sensor *sens) { this->reverse_active_energy = sens; }
void set_current_gain_calibration(int32_t val) { this->current_gain_calibration = val; }
void set_voltage_gain_calibration(int32_t val) { this->voltage_gain_calibration = val; }
void set_power_gain_calibration(int32_t val) { this->power_gain_calibration = val; }
void set_phase_angle_calibration(int32_t val) { this->phase_angle_calibration = val; }
sensor::Sensor *current{nullptr};
sensor::Sensor *voltage{nullptr};
sensor::Sensor *active_power{nullptr};
sensor::Sensor *apparent_power{nullptr};
sensor::Sensor *power_factor{nullptr};
sensor::Sensor *forward_active_energy{nullptr};
sensor::Sensor *reverse_active_energy{nullptr};
int32_t current_gain_calibration{0};
int32_t voltage_gain_calibration{0};
int32_t power_gain_calibration{0};
uint16_t phase_angle_calibration{0};
float forward_active_energy_total{0};
float reverse_active_energy_total{0};
};
// Store data in a class that doesn't use multiple-inheritance (no vtables in flash!)
struct ADE7880Store {
volatile bool reset_done{false};
bool reset_pending{false};
ISRInternalGPIOPin irq1_pin;
static void gpio_intr(ADE7880Store *arg);
};
class ADE7880 : public i2c::I2CDevice, public PollingComponent {
public:
void set_irq0_pin(InternalGPIOPin *pin) { this->irq0_pin_ = pin; }
void set_irq1_pin(InternalGPIOPin *pin) { this->irq1_pin_ = pin; }
void set_reset_pin(InternalGPIOPin *pin) { this->reset_pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
void set_channel_n(NeutralChannel *channel) { this->channel_n_ = channel; }
void set_channel_a(PowerChannel *channel) { this->channel_a_ = channel; }
void set_channel_b(PowerChannel *channel) { this->channel_b_ = channel; }
void set_channel_c(PowerChannel *channel) { this->channel_c_ = channel; }
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
ADE7880Store store_{};
InternalGPIOPin *irq0_pin_{nullptr};
InternalGPIOPin *irq1_pin_{nullptr};
InternalGPIOPin *reset_pin_{nullptr};
float frequency_;
NeutralChannel *channel_n_{nullptr};
PowerChannel *channel_a_{nullptr};
PowerChannel *channel_b_{nullptr};
PowerChannel *channel_c_{nullptr};
void calibrate_s10zp_reading_(uint16_t a_register, int16_t calibration);
void calibrate_s24zpse_reading_(uint16_t a_register, int32_t calibration);
void init_device_();
// each of these functions allow the caller to pass in a lambda (or any other callable)
// which modifies the value read from the register before it is passed to the sensor
// the callable will be passed a 'float' value and is expected to return a 'float'
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template<typename F> void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
void reset_device_();
uint8_t read_u8_register16_(uint16_t a_register);
int16_t read_s16_register16_(uint16_t a_register);
uint16_t read_u16_register16_(uint16_t a_register);
int32_t read_s24zp_register16_(uint16_t a_register);
int32_t read_s32_register16_(uint16_t a_register);
uint32_t read_u32_register16_(uint16_t a_register);
void write_u8_register16_(uint16_t a_register, uint8_t value);
void write_s10zp_register16_(uint16_t a_register, int16_t value);
void write_u16_register16_(uint16_t a_register, uint16_t value);
void write_s24zpse_register16_(uint16_t a_register, int32_t value);
void write_s32_register16_(uint16_t a_register, int32_t value);
void write_u32_register16_(uint16_t a_register, uint32_t value);
};
} // namespace ade7880
} // namespace esphome

View file

@ -0,0 +1,101 @@
// This component was developed using knowledge gathered by a number
// of people who reverse-engineered the Shelly 3EM:
//
// @AndreKR on GitHub
// Axel (@Axel830 on GitHub)
// Marko (@goodkiller on GitHub)
// Michaël Piron (@michaelpiron on GitHub)
// Theo Arends (@arendst on GitHub)
#include "ade7880.h"
namespace esphome {
namespace ade7880 {
// adapted from https://stackoverflow.com/a/55912127/1886371
template<size_t Bits, typename T> inline T sign_extend(const T &v) noexcept {
using S = struct { signed Val : Bits; };
return reinterpret_cast<const S *>(&v)->Val;
}
// Register types
// unsigned 8-bit (uint8_t)
// signed 10-bit - 16-bit ZP on wire (int16_t, needs sign extension)
// unsigned 16-bit (uint16_t)
// unsigned 20-bit - 32-bit ZP on wire (uint32_t)
// signed 24-bit - 32-bit ZPSE on wire (int32_t, needs sign extension)
// signed 24-bit - 32-bit ZP on wire (int32_t, needs sign extension)
// signed 24-bit - 32-bit SE on wire (int32_t)
// signed 28-bit - 32-bit ZP on wire (int32_t, needs sign extension)
// unsigned 32-bit (uint32_t)
// signed 32-bit (int32_t)
uint8_t ADE7880::read_u8_register16_(uint16_t a_register) {
uint8_t in;
this->read_register16(a_register, &in, sizeof(in));
return in;
}
int16_t ADE7880::read_s16_register16_(uint16_t a_register) {
int16_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
uint16_t ADE7880::read_u16_register16_(uint16_t a_register) {
uint16_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
int32_t ADE7880::read_s24zp_register16_(uint16_t a_register) {
// s24zp means 24 bit signed value in the lower 24 bits of a 32-bit register
int32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return sign_extend<24>(convert_big_endian(in));
}
int32_t ADE7880::read_s32_register16_(uint16_t a_register) {
int32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
uint32_t ADE7880::read_u32_register16_(uint16_t a_register) {
uint32_t in;
this->read_register16(a_register, reinterpret_cast<uint8_t *>(&in), sizeof(in));
return convert_big_endian(in);
}
void ADE7880::write_u8_register16_(uint16_t a_register, uint8_t value) {
this->write_register16(a_register, &value, sizeof(value));
}
void ADE7880::write_s10zp_register16_(uint16_t a_register, int16_t value) {
int16_t out = convert_big_endian(value & 0x03FF);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_u16_register16_(uint16_t a_register, uint16_t value) {
uint16_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_s24zpse_register16_(uint16_t a_register, int32_t value) {
// s24zpse means a 24-bit signed value, sign-extended to 28 bits, in the lower 28 bits of a 32-bit register
int32_t out = convert_big_endian(value & 0x0FFFFFFF);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_s32_register16_(uint16_t a_register, int32_t value) {
int32_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
void ADE7880::write_u32_register16_(uint16_t a_register, uint32_t value) {
uint32_t out = convert_big_endian(value);
this->write_register16(a_register, reinterpret_cast<uint8_t *>(&out), sizeof(out));
}
} // namespace ade7880
} // namespace esphome

View file

@ -0,0 +1,243 @@
#pragma once
// This file is a modified version of the one created by Michaël Piron (@michaelpiron on GitHub)
// Source: https://www.analog.com/media/en/technical-documentation/application-notes/AN-1127.pdf
namespace esphome {
namespace ade7880 {
// DSP Data Memory RAM registers
constexpr uint16_t AIGAIN = 0x4380;
constexpr uint16_t AVGAIN = 0x4381;
constexpr uint16_t BIGAIN = 0x4382;
constexpr uint16_t BVGAIN = 0x4383;
constexpr uint16_t CIGAIN = 0x4384;
constexpr uint16_t CVGAIN = 0x4385;
constexpr uint16_t NIGAIN = 0x4386;
constexpr uint16_t DICOEFF = 0x4388;
constexpr uint16_t APGAIN = 0x4389;
constexpr uint16_t AWATTOS = 0x438A;
constexpr uint16_t BPGAIN = 0x438B;
constexpr uint16_t BWATTOS = 0x438C;
constexpr uint16_t CPGAIN = 0x438D;
constexpr uint16_t CWATTOS = 0x438E;
constexpr uint16_t AIRMSOS = 0x438F;
constexpr uint16_t AVRMSOS = 0x4390;
constexpr uint16_t BIRMSOS = 0x4391;
constexpr uint16_t BVRMSOS = 0x4392;
constexpr uint16_t CIRMSOS = 0x4393;
constexpr uint16_t CVRMSOS = 0x4394;
constexpr uint16_t NIRMSOS = 0x4395;
constexpr uint16_t HPGAIN = 0x4398;
constexpr uint16_t ISUMLVL = 0x4399;
constexpr uint16_t VLEVEL = 0x439F;
constexpr uint16_t AFWATTOS = 0x43A2;
constexpr uint16_t BFWATTOS = 0x43A3;
constexpr uint16_t CFWATTOS = 0x43A4;
constexpr uint16_t AFVAROS = 0x43A5;
constexpr uint16_t BFVAROS = 0x43A6;
constexpr uint16_t CFVAROS = 0x43A7;
constexpr uint16_t AFIRMSOS = 0x43A8;
constexpr uint16_t BFIRMSOS = 0x43A9;
constexpr uint16_t CFIRMSOS = 0x43AA;
constexpr uint16_t AFVRMSOS = 0x43AB;
constexpr uint16_t BFVRMSOS = 0x43AC;
constexpr uint16_t CFVRMSOS = 0x43AD;
constexpr uint16_t HXWATTOS = 0x43AE;
constexpr uint16_t HYWATTOS = 0x43AF;
constexpr uint16_t HZWATTOS = 0x43B0;
constexpr uint16_t HXVAROS = 0x43B1;
constexpr uint16_t HYVAROS = 0x43B2;
constexpr uint16_t HZVAROS = 0x43B3;
constexpr uint16_t HXIRMSOS = 0x43B4;
constexpr uint16_t HYIRMSOS = 0x43B5;
constexpr uint16_t HZIRMSOS = 0x43B6;
constexpr uint16_t HXVRMSOS = 0x43B7;
constexpr uint16_t HYVRMSOS = 0x43B8;
constexpr uint16_t HZVRMSOS = 0x43B9;
constexpr uint16_t AIRMS = 0x43C0;
constexpr uint16_t AVRMS = 0x43C1;
constexpr uint16_t BIRMS = 0x43C2;
constexpr uint16_t BVRMS = 0x43C3;
constexpr uint16_t CIRMS = 0x43C4;
constexpr uint16_t CVRMS = 0x43C5;
constexpr uint16_t NIRMS = 0x43C6;
constexpr uint16_t ISUM = 0x43C7;
// Internal DSP Memory RAM registers
constexpr uint16_t RUN = 0xE228;
constexpr uint16_t AWATTHR = 0xE400;
constexpr uint16_t BWATTHR = 0xE401;
constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;
constexpr uint16_t AVAHR = 0xE40C;
constexpr uint16_t BVAHR = 0xE40D;
constexpr uint16_t CVAHR = 0xE40E;
constexpr uint16_t IPEAK = 0xE500;
constexpr uint16_t VPEAK = 0xE501;
constexpr uint16_t STATUS0 = 0xE502;
constexpr uint16_t STATUS1 = 0xE503;
constexpr uint16_t AIMAV = 0xE504;
constexpr uint16_t BIMAV = 0xE505;
constexpr uint16_t CIMAV = 0xE506;
constexpr uint16_t OILVL = 0xE507;
constexpr uint16_t OVLVL = 0xE508;
constexpr uint16_t SAGLVL = 0xE509;
constexpr uint16_t MASK0 = 0xE50A;
constexpr uint16_t MASK1 = 0xE50B;
constexpr uint16_t IAWV = 0xE50C;
constexpr uint16_t IBWV = 0xE50D;
constexpr uint16_t ICWV = 0xE50E;
constexpr uint16_t INWV = 0xE50F;
constexpr uint16_t VAWV = 0xE510;
constexpr uint16_t VBWV = 0xE511;
constexpr uint16_t VCWV = 0xE512;
constexpr uint16_t AWATT = 0xE513;
constexpr uint16_t BWATT = 0xE514;
constexpr uint16_t CWATT = 0xE515;
constexpr uint16_t AFVAR = 0xE516;
constexpr uint16_t BFVAR = 0xE517;
constexpr uint16_t CFVAR = 0xE518;
constexpr uint16_t AVA = 0xE519;
constexpr uint16_t BVA = 0xE51A;
constexpr uint16_t CVA = 0xE51B;
constexpr uint16_t CHECKSUM = 0xE51F;
constexpr uint16_t VNOM = 0xE520;
constexpr uint16_t LAST_RWDATA_24BIT = 0xE5FF;
constexpr uint16_t PHSTATUS = 0xE600;
constexpr uint16_t ANGLE0 = 0xE601;
constexpr uint16_t ANGLE1 = 0xE602;
constexpr uint16_t ANGLE2 = 0xE603;
constexpr uint16_t PHNOLOAD = 0xE608;
constexpr uint16_t LINECYC = 0xE60C;
constexpr uint16_t ZXTOUT = 0xE60D;
constexpr uint16_t COMPMODE = 0xE60E;
constexpr uint16_t GAIN = 0xE60F;
constexpr uint16_t CFMODE = 0xE610;
constexpr uint16_t CF1DEN = 0xE611;
constexpr uint16_t CF2DEN = 0xE612;
constexpr uint16_t CF3DEN = 0xE613;
constexpr uint16_t APHCAL = 0xE614;
constexpr uint16_t BPHCAL = 0xE615;
constexpr uint16_t CPHCAL = 0xE616;
constexpr uint16_t PHSIGN = 0xE617;
constexpr uint16_t CONFIG = 0xE618;
constexpr uint16_t MMODE = 0xE700;
constexpr uint16_t ACCMODE = 0xE701;
constexpr uint16_t LCYCMODE = 0xE702;
constexpr uint16_t PEAKCYC = 0xE703;
constexpr uint16_t SAGCYC = 0xE704;
constexpr uint16_t CFCYC = 0xE705;
constexpr uint16_t HSDC_CFG = 0xE706;
constexpr uint16_t VERSION = 0xE707;
constexpr uint16_t DSPWP_SET = 0xE7E3;
constexpr uint16_t LAST_RWDATA_8BIT = 0xE7FD;
constexpr uint16_t DSPWP_SEL = 0xE7FE;
constexpr uint16_t FVRMS = 0xE880;
constexpr uint16_t FIRMS = 0xE881;
constexpr uint16_t FWATT = 0xE882;
constexpr uint16_t FVAR = 0xE883;
constexpr uint16_t FVA = 0xE884;
constexpr uint16_t FPF = 0xE885;
constexpr uint16_t VTHDN = 0xE886;
constexpr uint16_t ITHDN = 0xE887;
constexpr uint16_t HXVRMS = 0xE888;
constexpr uint16_t HXIRMS = 0xE889;
constexpr uint16_t HXWATT = 0xE88A;
constexpr uint16_t HXVAR = 0xE88B;
constexpr uint16_t HXVA = 0xE88C;
constexpr uint16_t HXPF = 0xE88D;
constexpr uint16_t HXVHD = 0xE88E;
constexpr uint16_t HXIHD = 0xE88F;
constexpr uint16_t HYVRMS = 0xE890;
constexpr uint16_t HYIRMS = 0xE891;
constexpr uint16_t HYWATT = 0xE892;
constexpr uint16_t HYVAR = 0xE893;
constexpr uint16_t HYVA = 0xE894;
constexpr uint16_t HYPF = 0xE895;
constexpr uint16_t HYVHD = 0xE896;
constexpr uint16_t HYIHD = 0xE897;
constexpr uint16_t HZVRMS = 0xE898;
constexpr uint16_t HZIRMS = 0xE899;
constexpr uint16_t HZWATT = 0xE89A;
constexpr uint16_t HZVAR = 0xE89B;
constexpr uint16_t HZVA = 0xE89C;
constexpr uint16_t HZPF = 0xE89D;
constexpr uint16_t HZVHD = 0xE89E;
constexpr uint16_t HZIHD = 0xE89F;
constexpr uint16_t HCONFIG = 0xE900;
constexpr uint16_t APF = 0xE902;
constexpr uint16_t BPF = 0xE903;
constexpr uint16_t CPF = 0xE904;
constexpr uint16_t APERIOD = 0xE905;
constexpr uint16_t BPERIOD = 0xE906;
constexpr uint16_t CPERIOD = 0xE907;
constexpr uint16_t APNOLOAD = 0xE908;
constexpr uint16_t VARNOLOAD = 0xE909;
constexpr uint16_t VANOLOAD = 0xE90A;
constexpr uint16_t LAST_ADD = 0xE9FE;
constexpr uint16_t LAST_RWDATA_16BIT = 0xE9FF;
constexpr uint16_t CONFIG3 = 0xEA00;
constexpr uint16_t LAST_OP = 0xEA01;
constexpr uint16_t WTHR = 0xEA02;
constexpr uint16_t VARTHR = 0xEA03;
constexpr uint16_t VATHR = 0xEA04;
constexpr uint16_t HX_REG = 0xEA08;
constexpr uint16_t HY_REG = 0xEA09;
constexpr uint16_t HZ_REG = 0xEA0A;
constexpr uint16_t LPOILVL = 0xEC00;
constexpr uint16_t CONFIG2 = 0xEC01;
// STATUS1 Register Bits
constexpr uint32_t STATUS1_RSTDONE = (1 << 15);
// CONFIG Register Bits
constexpr uint16_t CONFIG_SWRST = (1 << 7);
// CONFIG2 Register Bits
constexpr uint8_t CONFIG2_I2C_LOCK = (1 << 1);
// COMPMODE Register Bits
constexpr uint16_t COMPMODE_DEFAULT = 0x01FF;
constexpr uint16_t COMPMODE_SELFREQ = (1 << 14);
// RUN Register Bits
constexpr uint16_t RUN_ENABLE = (1 << 0);
// DSPWP_SET Register Bits
constexpr uint8_t DSPWP_SET_RO = (1 << 7);
// DSPWP_SEL Register Bits
constexpr uint8_t DSPWP_SEL_SET = 0xAD;
} // namespace ade7880
} // namespace esphome

View file

@ -0,0 +1,290 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, i2c
from esphome import pins
from esphome.const import (
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_CALIBRATION,
CONF_CURRENT,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_FREQUENCY,
CONF_ID,
CONF_NAME,
CONF_PHASE_A,
CONF_PHASE_ANGLE,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_POWER_FACTOR,
CONF_RESET_PIN,
CONF_REVERSE_ACTIVE_ENERGY,
CONF_VOLTAGE,
CONF_VOLTAGE_GAIN,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_PERCENT,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
DEPENDENCIES = ["i2c"]
ade7880_ns = cg.esphome_ns.namespace("ade7880")
ADE7880 = ade7880_ns.class_("ADE7880", cg.PollingComponent, i2c.I2CDevice)
NeutralChannel = ade7880_ns.struct("NeutralChannel")
PowerChannel = ade7880_ns.struct("PowerChannel")
CONF_CURRENT_GAIN = "current_gain"
CONF_IRQ0_PIN = "irq0_pin"
CONF_IRQ1_PIN = "irq1_pin"
CONF_POWER_GAIN = "power_gain"
CONF_NEUTRAL = "neutral"
NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(NeutralChannel),
cv.Optional(CONF_NAME): cv.string_strict,
cv.Required(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Required(CONF_CALIBRATION): cv.Schema(
{
cv.Required(CONF_CURRENT_GAIN): cv.int_,
},
),
}
)
POWER_CHANNEL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(PowerChannel),
cv.Optional(CONF_NAME): cv.string_strict,
cv.Optional(CONF_VOLTAGE): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_CURRENT): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_ACTIVE_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_APPARENT_POWER): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_POWER_FACTOR): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
),
cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
cv.Required(CONF_CALIBRATION): cv.Schema(
{
cv.Required(CONF_CURRENT_GAIN): cv.int_,
cv.Required(CONF_VOLTAGE_GAIN): cv.int_,
cv.Required(CONF_POWER_GAIN): cv.int_,
cv.Required(CONF_PHASE_ANGLE): cv.int_,
},
),
}
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_PHASE_A): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_PHASE_B): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_PHASE_C): POWER_CHANNEL_SCHEMA,
cv.Optional(CONF_NEUTRAL): NEUTRAL_CHANNEL_SCHEMA,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x38))
)
async def neutral_channel(config):
var = cg.new_Pvariable(config[CONF_ID])
current = config[CONF_CURRENT]
sens = await sensor.new_sensor(current)
cg.add(var.set_current(sens))
cg.add(
var.set_current_gain_calibration(config[CONF_CALIBRATION][CONF_CURRENT_GAIN])
)
return var
async def power_channel(config):
var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := config.get(sensor_type):
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{sensor_type}")(sens))
for calib_type in [
CONF_CURRENT_GAIN,
CONF_VOLTAGE_GAIN,
CONF_POWER_GAIN,
CONF_PHASE_ANGLE,
]:
cg.add(
getattr(var, f"set_{calib_type}_calibration")(
config[CONF_CALIBRATION][calib_type]
)
)
return var
def final_validate(config):
for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
if channel := config.get(channel):
channel_name = channel.get(CONF_NAME)
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := channel.get(sensor_type):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
if channel := config.get(CONF_NEUTRAL):
channel_name = channel.get(CONF_NAME)
if conf := channel.get(CONF_CURRENT):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if irq0_pin := config.get(CONF_IRQ0_PIN):
pin = await cg.gpio_pin_expression(irq0_pin)
cg.add(var.set_irq0_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_IRQ1_PIN])
cg.add(var.set_irq1_pin(pin))
if reset_pin := config.get(CONF_RESET_PIN):
pin = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(pin))
if frequency := config.get(CONF_FREQUENCY):
cg.add(var.set_frequency(frequency))
if channel := config.get(CONF_PHASE_A):
chan = await power_channel(channel)
cg.add(var.set_channel_a(chan))
if channel := config.get(CONF_PHASE_B):
chan = await power_channel(channel)
cg.add(var.set_channel_b(chan))
if channel := config.get(CONF_PHASE_C):
chan = await power_channel(channel)
cg.add(var.set_channel_c(chan))
if channel := config.get(CONF_NEUTRAL):
chan = await neutral_channel(channel)
cg.add(var.set_channel_n(chan))

View file

@ -1,5 +1,5 @@
import esphome.config_validation as cv
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
CONFIG_SCHEMA = cv.invalid(
"The ade7953 sensor component has been renamed to ade7953_i2c."
)

View file

@ -6,6 +6,7 @@ from esphome.const import (
CONF_IRQ_PIN,
CONF_VOLTAGE,
CONF_FREQUENCY,
CONF_VOLTAGE_GAIN,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_POWER,
@ -36,11 +37,11 @@ CONF_POWER_FACTOR_B = "power_factor_b"
CONF_VOLTAGE_PGA_GAIN = "voltage_pga_gain"
CONF_CURRENT_PGA_GAIN_A = "current_pga_gain_a"
CONF_CURRENT_PGA_GAIN_B = "current_pga_gain_b"
CONF_VOLTAGE_GAIN = "voltage_gain"
CONF_CURRENT_GAIN_A = "current_gain_a"
CONF_CURRENT_GAIN_B = "current_gain_b"
CONF_ACTIVE_POWER_GAIN_A = "active_power_gain_a"
CONF_ACTIVE_POWER_GAIN_B = "active_power_gain_b"
CONF_USE_ACCUMULATED_ENERGY_REGISTERS = "use_accumulated_energy_registers"
PGA_GAINS = {
"1x": 0b000,
"2x": 0b001,
@ -155,6 +156,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_ACTIVE_POWER_GAIN_B, default=0x400000): cv.hex_int_range(
min=0x100000, max=0x800000
),
cv.Optional(CONF_USE_ACCUMULATED_ENERGY_REGISTERS, default=False): cv.boolean,
}
).extend(cv.polling_component_schema("60s"))
@ -174,6 +176,9 @@ async def register_ade7953(var, config):
cg.add(var.set_bigain(config.get(CONF_CURRENT_GAIN_B)))
cg.add(var.set_awgain(config.get(CONF_ACTIVE_POWER_GAIN_A)))
cg.add(var.set_bwgain(config.get(CONF_ACTIVE_POWER_GAIN_B)))
cg.add(
var.set_use_acc_energy_regs(config.get(CONF_USE_ACCUMULATED_ENERGY_REGISTERS))
)
for key in [
CONF_VOLTAGE,

View file

@ -1,11 +1,16 @@
#include "ade7953_base.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace ade7953_base {
static const char *const TAG = "ade7953";
static const float ADE_POWER_FACTOR = 154.0f;
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
void ADE7953::setup() {
if (this->irq_pin_ != nullptr) {
this->irq_pin_->setup();
@ -34,6 +39,7 @@ void ADE7953::setup() {
this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_);
this->ade_read_32(BWGAIN_32, &bwgain_);
this->last_update_ = millis();
this->is_setup_ = true;
});
}
@ -52,6 +58,7 @@ void ADE7953::dump_config() {
LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_);
LOG_SENSOR(" ", "Rective Power A Sensor", this->reactive_power_a_sensor_);
LOG_SENSOR(" ", "Reactive Power B Sensor", this->reactive_power_b_sensor_);
ESP_LOGCONFIG(TAG, " USE_ACC_ENERGY_REGS: %d", this->use_acc_energy_regs_);
ESP_LOGCONFIG(TAG, " PGA_V_8: 0x%X", pga_v_);
ESP_LOGCONFIG(TAG, " PGA_IA_8: 0x%X", pga_ia_);
ESP_LOGCONFIG(TAG, " PGA_IB_8: 0x%X", pga_ib_);
@ -85,6 +92,7 @@ void ADE7953::update() {
uint32_t val;
uint16_t val_16;
uint16_t reg;
// Power factor
err = this->ade_read_16(0x010A, &val_16);
@ -92,23 +100,36 @@ void ADE7953::update() {
err = this->ade_read_16(0x010B, &val_16);
ADE_PUBLISH(power_factor_b, (int16_t) val_16, (0x7FFF / 100.0f));
float pf = ADE_POWER_FACTOR;
if (this->use_acc_energy_regs_) {
const uint32_t now = millis();
const auto diff = now - this->last_update_;
this->last_update_ = now;
// prevent DIV/0
pf = ADE_WATTSEC_POWER_FACTOR * (diff < 10 ? 10 : diff) / 1000;
ESP_LOGVV(TAG, "ADE7953::update() diff=%" PRIu32 " pf=%f", diff, pf);
}
// Apparent power
err = this->ade_read_32(0x0310, &val);
ADE_PUBLISH(apparent_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0311, &val);
ADE_PUBLISH(apparent_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x0322 : 0x0310;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(apparent_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(apparent_power_b, (int32_t) val, pf);
// Active power
err = this->ade_read_32(0x0312, &val);
ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0313, &val);
ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x031E : 0x0312;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(active_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(active_power_b, (int32_t) val, pf);
// Reactive power
err = this->ade_read_32(0x0314, &val);
ADE_PUBLISH(reactive_power_a, (int32_t) val, 154.0f);
err = this->ade_read_32(0x0315, &val);
ADE_PUBLISH(reactive_power_b, (int32_t) val, 154.0f);
reg = this->use_acc_energy_regs_ ? 0x0320 : 0x0314;
err = this->ade_read_32(reg, &val);
ADE_PUBLISH(reactive_power_a, (int32_t) val, pf);
err = this->ade_read_32(reg + 1, &val);
ADE_PUBLISH(reactive_power_b, (int32_t) val, pf);
// Current
err = this->ade_read_32(0x031A, &val);

View file

@ -52,6 +52,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
void set_bwgain(uint32_t bwgain) { bwgain_ = bwgain; }
void set_use_acc_energy_regs(bool use_acc_energy_regs) { use_acc_energy_regs_ = use_acc_energy_regs; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
@ -103,6 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint32_t bigain_;
uint32_t awgain_;
uint32_t bwgain_;
bool use_acc_energy_regs_{false};
uint32_t last_update_;
virtual bool ade_write_8(uint16_t reg, uint8_t value) = 0;

View file

@ -13,29 +13,29 @@ void AdE7953I2c::dump_config() {
ade7953_base::ADE7953::dump_config();
}
bool AdE7953I2c::ade_write_8(uint16_t reg, uint8_t value) {
std::vector<uint8_t> data(3);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[3];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value;
return this->write(data, 3) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_16(uint16_t reg, uint16_t value) {
std::vector<uint8_t> data(4);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value >> 8);
data.push_back(value >> 0);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[4];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 8;
data[3] = value >> 0;
return this->write(data, 4) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_32(uint16_t reg, uint32_t value) {
std::vector<uint8_t> data(6);
data.push_back(reg >> 8);
data.push_back(reg >> 0);
data.push_back(value >> 24);
data.push_back(value >> 16);
data.push_back(value >> 8);
data.push_back(value >> 0);
return this->write(data.data(), data.size()) != i2c::ERROR_OK;
uint8_t data[6];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 24;
data[3] = value >> 16;
data[4] = value >> 8;
data[5] = value >> 0;
return this->write(data, 6) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
uint8_t reg_data[2];

View file

@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) {
this->write_byte16(reg);
this->transfer_byte(0x80);
uint8_t recv[2];
this->read_array(recv, 4);
this->read_array(recv, 2);
*value = encode_uint16(recv[0], recv[1]);
this->disable();
return false;

View file

@ -4,13 +4,14 @@ from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "voltage_sampler"]
MULTI_CONF = True
ads1115_ns = cg.esphome_ns.namespace("ads1115")
ADS1115Component = ads1115_ns.class_("ADS1115Component", cg.Component, i2c.I2CDevice)
CONF_CONTINUOUS_MODE = "continuous_mode"
CONF_ADS1115_ID = "ads1115_id"
CONFIG_SCHEMA = (
cv.Schema(
{

View file

@ -1,6 +1,6 @@
#include "ads1115.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ads1115 {
@ -75,25 +75,19 @@ void ADS1115Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with ADS1115 failed!");
}
for (auto *sensor : this->sensors_) {
LOG_SENSOR(" ", "Sensor", sensor);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", sensor->get_multiplexer());
ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain());
ESP_LOGCONFIG(TAG, " Resolution: %u", sensor->get_resolution());
}
}
float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
ADS1115Resolution resolution) {
uint16_t config = this->prev_config_;
// Multiplexer
// 0bxBBBxxxxxxxxxxxx
config &= 0b1000111111111111;
config |= (sensor->get_multiplexer() & 0b111) << 12;
config |= (multiplexer & 0b111) << 12;
// Gain
// 0bxxxxBBBxxxxxxxxx
config &= 0b1111000111111111;
config |= (sensor->get_gain() & 0b111) << 9;
config |= (gain & 0b111) << 9;
if (!this->continuous_mode_) {
// Start conversion
@ -132,7 +126,7 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
return NAN;
}
if (sensor->get_resolution() == ADS1015_12_BITS) {
if (resolution == ADS1015_12_BITS) {
bool negative = (raw_conversion >> 15) == 1;
// shift raw_conversion as it's only 12-bits, left justified
@ -151,8 +145,8 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
auto signed_conversion = static_cast<int16_t>(raw_conversion);
float millivolts;
float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f;
switch (sensor->get_gain()) {
float divider = (resolution == ADS1115_16_BITS) ? 32768.0f : 2048.0f;
switch (gain) {
case ADS1115_GAIN_6P144:
millivolts = (signed_conversion * 6144) / divider;
break;
@ -179,14 +173,5 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
return millivolts / 1e3f;
}
float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); }
void ADS1115Sensor::update() {
float v = this->parent_->request_measurement(this);
if (!std::isnan(v)) {
ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v);
this->publish_state(v);
}
}
} // namespace ads1115
} // namespace esphome

View file

@ -1,9 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include <vector>
@ -35,12 +33,8 @@ enum ADS1115Resolution {
ADS1015_12_BITS = 12,
};
class ADS1115Sensor;
class ADS1115Component : public Component, public i2c::I2CDevice {
public:
void register_sensor(ADS1115Sensor *obj) { this->sensors_.push_back(obj); }
/// Set up the internal sensor array.
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
@ -48,33 +42,12 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
/// Helper method to request a measurement from a sensor.
float request_measurement(ADS1115Sensor *sensor);
float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution);
protected:
std::vector<ADS1115Sensor *> sensors_;
uint16_t prev_config_{0};
bool continuous_mode_;
};
/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors.
class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
ADS1115Sensor(ADS1115Component *parent) : parent_(parent) {}
void update() override;
void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; }
void set_gain(ADS1115Gain gain) { gain_ = gain; }
void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; }
float sample() override;
uint8_t get_multiplexer() const { return multiplexer_; }
uint8_t get_gain() const { return gain_; }
uint8_t get_resolution() const { return resolution_; }
protected:
ADS1115Component *parent_;
ADS1115Multiplexer multiplexer_;
ADS1115Gain gain_;
ADS1115Resolution resolution_;
};
} // namespace ads1115
} // namespace esphome

View file

@ -1,92 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import (
CONF_GAIN,
CONF_MULTIPLEXER,
CONF_RESOLUTION,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
CONF_ID,
)
from . import ads1115_ns, ADS1115Component
DEPENDENCIES = ["ads1115"]
ADS1115Multiplexer = ads1115_ns.enum("ADS1115Multiplexer")
MUX = {
"A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1,
"A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3,
"A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3,
"A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3,
"A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG,
"A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG,
"A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG,
"A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG,
}
ADS1115Gain = ads1115_ns.enum("ADS1115Gain")
GAIN = {
"6.144": ADS1115Gain.ADS1115_GAIN_6P144,
"4.096": ADS1115Gain.ADS1115_GAIN_4P096,
"2.048": ADS1115Gain.ADS1115_GAIN_2P048,
"1.024": ADS1115Gain.ADS1115_GAIN_1P024,
"0.512": ADS1115Gain.ADS1115_GAIN_0P512,
"0.256": ADS1115Gain.ADS1115_GAIN_0P256,
}
ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution")
RESOLUTION = {
"16_BITS": ADS1115Resolution.ADS1115_16_BITS,
"12_BITS": ADS1115Resolution.ADS1015_12_BITS,
}
def validate_gain(value):
if isinstance(value, float):
value = f"{value:0.03f}"
elif not isinstance(value, str):
raise cv.Invalid(f'invalid gain "{value}"')
return cv.enum(GAIN)(value)
ADS1115Sensor = ads1115_ns.class_(
"ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
CONF_ADS1115_ID = "ads1115_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
ADS1115Sensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component),
cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"),
cv.Required(CONF_GAIN): validate_gain,
cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum(
RESOLUTION, upper=True, space="_"
),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
paren = await cg.get_variable(config[CONF_ADS1115_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
await sensor.register_sensor(var, config)
await cg.register_component(var, config)
cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
cg.add(var.set_gain(config[CONF_GAIN]))
cg.add(var.set_resolution(config[CONF_RESOLUTION]))
cg.add(paren.register_sensor(var))

View file

@ -0,0 +1,81 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import (
CONF_GAIN,
CONF_MULTIPLEXER,
CONF_RESOLUTION,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
CONF_ID,
)
from .. import ads1115_ns, ADS1115Component, CONF_ADS1115_ID
AUTO_LOAD = ["voltage_sampler"]
DEPENDENCIES = ["ads1115"]
ADS1115Multiplexer = ads1115_ns.enum("ADS1115Multiplexer")
MUX = {
"A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1,
"A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3,
"A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3,
"A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3,
"A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG,
"A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG,
"A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG,
"A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG,
}
ADS1115Gain = ads1115_ns.enum("ADS1115Gain")
GAIN = {
"6.144": ADS1115Gain.ADS1115_GAIN_6P144,
"4.096": ADS1115Gain.ADS1115_GAIN_4P096,
"2.048": ADS1115Gain.ADS1115_GAIN_2P048,
"1.024": ADS1115Gain.ADS1115_GAIN_1P024,
"0.512": ADS1115Gain.ADS1115_GAIN_0P512,
"0.256": ADS1115Gain.ADS1115_GAIN_0P256,
}
ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution")
RESOLUTION = {
"16_BITS": ADS1115Resolution.ADS1115_16_BITS,
"12_BITS": ADS1115Resolution.ADS1015_12_BITS,
}
ADS1115Sensor = ads1115_ns.class_(
"ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
ADS1115Sensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component),
cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"),
cv.Required(CONF_GAIN): cv.enum(GAIN, string=True),
cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum(
RESOLUTION, upper=True, space="_"
),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await sensor.register_sensor(var, config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ADS1115_ID])
cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
cg.add(var.set_gain(config[CONF_GAIN]))
cg.add(var.set_resolution(config[CONF_RESOLUTION]))

View file

@ -0,0 +1,30 @@
#include "ads1115_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ads1115 {
static const char *const TAG = "ads1115.sensor";
float ADS1115Sensor::sample() {
return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_);
}
void ADS1115Sensor::update() {
float v = this->sample();
if (!std::isnan(v)) {
ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v);
this->publish_state(v);
}
}
void ADS1115Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1115 Sensor", this);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_);
}
} // namespace ads1115
} // namespace esphome

View file

@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "../ads1115.h"
namespace esphome {
namespace ads1115 {
/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors.
class ADS1115Sensor : public sensor::Sensor,
public PollingComponent,
public voltage_sampler::VoltageSampler,
public Parented<ADS1115Component> {
public:
void update() override;
void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
void set_gain(ADS1115Gain gain) { this->gain_ = gain; }
void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; }
float sample() override;
void dump_config() override;
protected:
ADS1115Multiplexer multiplexer_;
ADS1115Gain gain_;
ADS1115Resolution resolution_;
};
} // namespace ads1115
} // namespace esphome

View file

@ -0,0 +1,25 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import spi
from esphome.const import CONF_ID
CODEOWNERS = ["@solomondg1"]
DEPENDENCIES = ["spi"]
MULTI_CONF = True
CONF_ADS1118_ID = "ads1118_id"
ads1118_ns = cg.esphome_ns.namespace("ads1118")
ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADS1118),
}
).extend(spi.spi_device_schema(cs_pin_required=True))
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)

View file

@ -0,0 +1,126 @@
#include "ads1118.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ads1118 {
static const char *const TAG = "ads1118";
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
void ADS1118::setup() {
ESP_LOGCONFIG(TAG, "Setting up ads1118");
this->spi_setup();
this->config_ = 0;
// Setup multiplexer
// 0bx000xxxxxxxxxxxx
this->config_ |= ADS1118_MULTIPLEXER_P0_NG << 12;
// Setup Gain
// 0bxxxx000xxxxxxxxx
this->config_ |= ADS1118_GAIN_6P144 << 9;
// Set singleshot mode
// 0bxxxxxxx1xxxxxxxx
this->config_ |= 0b0000000100000000;
// Set data rate - 860 samples per second (we're in singleshot mode)
// 0bxxxxxxxx100xxxxx
this->config_ |= ADS1118_DATA_RATE_860_SPS << 5;
// Set temperature sensor mode - ADC
// 0bxxxxxxxxxxx0xxxx
this->config_ |= 0b0000000000000000;
// Set DOUT pull up - enable
// 0bxxxxxxxxxxxx0xxx
this->config_ |= 0b0000000000001000;
// NOP - must be 01
// 0bxxxxxxxxxxxxx01x
this->config_ |= 0b0000000000000010;
// Not used - can be 0 or 1, lets be positive
// 0bxxxxxxxxxxxxxxx1
this->config_ |= 0b0000000000000001;
}
void ADS1118::dump_config() {
ESP_LOGCONFIG(TAG, "ADS1118:");
LOG_PIN(" CS Pin:", this->cs_);
}
float ADS1118::request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode) {
uint16_t temp_config = this->config_;
// Multiplexer
// 0bxBBBxxxxxxxxxxxx
temp_config &= 0b1000111111111111;
temp_config |= (multiplexer & 0b111) << 12;
// Gain
// 0bxxxxBBBxxxxxxxxx
temp_config &= 0b1111000111111111;
temp_config |= (gain & 0b111) << 9;
if (temperature_mode) {
// Set temperature sensor mode
// 0bxxxxxxxxxxx1xxxx
temp_config |= 0b0000000000010000;
} else {
// Set ADC mode
// 0bxxxxxxxxxxx0xxxx
temp_config &= 0b1111111111101111;
}
// Start conversion
temp_config |= 0b1000000000000000;
this->enable();
this->write_byte16(temp_config);
this->disable();
// about 1.2 ms with 860 samples per second
delay(2);
this->enable();
uint8_t adc_first_byte = this->read_byte();
uint8_t adc_second_byte = this->read_byte();
this->disable();
uint16_t raw_conversion = encode_uint16(adc_first_byte, adc_second_byte);
auto signed_conversion = static_cast<int16_t>(raw_conversion);
if (temperature_mode) {
return (signed_conversion >> 2) * 0.03125f;
} else {
float millivolts;
float divider = 32768.0f;
switch (gain) {
case ADS1118_GAIN_6P144:
millivolts = (signed_conversion * 6144) / divider;
break;
case ADS1118_GAIN_4P096:
millivolts = (signed_conversion * 4096) / divider;
break;
case ADS1118_GAIN_2P048:
millivolts = (signed_conversion * 2048) / divider;
break;
case ADS1118_GAIN_1P024:
millivolts = (signed_conversion * 1024) / divider;
break;
case ADS1118_GAIN_0P512:
millivolts = (signed_conversion * 512) / divider;
break;
case ADS1118_GAIN_0P256:
millivolts = (signed_conversion * 256) / divider;
break;
default:
millivolts = NAN;
}
return millivolts / 1e3f;
}
}
} // namespace ads1118
} // namespace esphome

View file

@ -0,0 +1,46 @@
#pragma once
#include "esphome/components/spi/spi.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace ads1118 {
enum ADS1118Multiplexer {
ADS1118_MULTIPLEXER_P0_N1 = 0b000,
ADS1118_MULTIPLEXER_P0_N3 = 0b001,
ADS1118_MULTIPLEXER_P1_N3 = 0b010,
ADS1118_MULTIPLEXER_P2_N3 = 0b011,
ADS1118_MULTIPLEXER_P0_NG = 0b100,
ADS1118_MULTIPLEXER_P1_NG = 0b101,
ADS1118_MULTIPLEXER_P2_NG = 0b110,
ADS1118_MULTIPLEXER_P3_NG = 0b111,
};
enum ADS1118Gain {
ADS1118_GAIN_6P144 = 0b000,
ADS1118_GAIN_4P096 = 0b001,
ADS1118_GAIN_2P048 = 0b010,
ADS1118_GAIN_1P024 = 0b011,
ADS1118_GAIN_0P512 = 0b100,
ADS1118_GAIN_0P256 = 0b101,
};
class ADS1118 : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
ADS1118() = default;
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/// Helper method to request a measurement from a sensor.
float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode);
protected:
uint16_t config_{0};
};
} // namespace ads1118
} // namespace esphome

View file

@ -0,0 +1,97 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import (
CONF_GAIN,
CONF_MULTIPLEXER,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_VOLT,
CONF_TYPE,
)
from .. import ads1118_ns, ADS1118, CONF_ADS1118_ID
AUTO_LOAD = ["voltage_sampler"]
DEPENDENCIES = ["ads1118"]
ADS1118Multiplexer = ads1118_ns.enum("ADS1118Multiplexer")
MUX = {
"A0_A1": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N1,
"A0_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_N3,
"A1_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_N3,
"A2_A3": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_N3,
"A0_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P0_NG,
"A1_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P1_NG,
"A2_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P2_NG,
"A3_GND": ADS1118Multiplexer.ADS1118_MULTIPLEXER_P3_NG,
}
ADS1118Gain = ads1118_ns.enum("ADS1118Gain")
GAIN = {
"6.144": ADS1118Gain.ADS1118_GAIN_6P144,
"4.096": ADS1118Gain.ADS1118_GAIN_4P096,
"2.048": ADS1118Gain.ADS1118_GAIN_2P048,
"1.024": ADS1118Gain.ADS1118_GAIN_1P024,
"0.512": ADS1118Gain.ADS1118_GAIN_0P512,
"0.256": ADS1118Gain.ADS1118_GAIN_0P256,
}
ADS1118Sensor = ads1118_ns.class_(
"ADS1118Sensor",
cg.PollingComponent,
sensor.Sensor,
voltage_sampler.VoltageSampler,
cg.Parented.template(ADS1118),
)
TYPE_ADC = "adc"
TYPE_TEMPERATURE = "temperature"
CONFIG_SCHEMA = cv.typed_schema(
{
TYPE_ADC: sensor.sensor_schema(
ADS1118Sensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118),
cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"),
cv.Required(CONF_GAIN): cv.enum(GAIN, string=True),
}
)
.extend(cv.polling_component_schema("60s")),
TYPE_TEMPERATURE: sensor.sensor_schema(
ADS1118Sensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_ADS1118_ID): cv.use_id(ADS1118),
}
)
.extend(cv.polling_component_schema("60s")),
},
default_type=TYPE_ADC,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ADS1118_ID])
if config[CONF_TYPE] == TYPE_ADC:
cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
cg.add(var.set_gain(config[CONF_GAIN]))
if config[CONF_TYPE] == TYPE_TEMPERATURE:
cg.add(var.set_temperature_mode(True))

View file

@ -0,0 +1,29 @@
#include "ads1118_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ads1118 {
static const char *const TAG = "ads1118.sensor";
void ADS1118Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1118 Sensor", this);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
}
float ADS1118Sensor::sample() {
return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->temperature_mode_);
}
void ADS1118Sensor::update() {
float v = this->sample();
if (!std::isnan(v)) {
ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v);
this->publish_state(v);
}
}
} // namespace ads1118
} // namespace esphome

View file

@ -0,0 +1,36 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "../ads1118.h"
namespace esphome {
namespace ads1118 {
class ADS1118Sensor : public PollingComponent,
public sensor::Sensor,
public voltage_sampler::VoltageSampler,
public Parented<ADS1118> {
public:
void update() override;
void set_multiplexer(ADS1118Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
void set_gain(ADS1118Gain gain) { this->gain_ = gain; }
void set_temperature_mode(bool temp) { this->temperature_mode_ = temp; }
float sample() override;
void dump_config() override;
protected:
ADS1118Multiplexer multiplexer_{ADS1118_MULTIPLEXER_P0_NG};
ADS1118Gain gain_{ADS1118_GAIN_6P144};
bool temperature_mode_;
};
} // namespace ads1118
} // namespace esphome

View file

@ -0,0 +1 @@
CODEOWNERS = ["@mak-42"]

View file

@ -0,0 +1,214 @@
#include "ags10.h"
#include <cinttypes>
namespace esphome {
namespace ags10 {
static const char *const TAG = "ags10";
// Data acquisition.
static const uint8_t REG_TVOC = 0x00;
// Zero-point calibration.
static const uint8_t REG_CALIBRATION = 0x01;
// Read version.
static const uint8_t REG_VERSION = 0x11;
// Read current resistance.
static const uint8_t REG_RESISTANCE = 0x20;
// Modify target address.
static const uint8_t REG_ADDRESS = 0x21;
// Zero-point calibration with current resistance.
static const uint16_t ZP_CURRENT = 0x0000;
// Zero-point reset.
static const uint16_t ZP_DEFAULT = 0xFFFF;
void AGS10Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ags10...");
auto version = this->read_version_();
if (version) {
ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version);
if (this->version_ != nullptr) {
this->version_->publish_state(*version);
}
} else {
ESP_LOGE(TAG, "AGS10 Sensor Version: unknown");
}
auto resistance = this->read_resistance_();
if (resistance) {
ESP_LOGD(TAG, "AGS10 Sensor Resistance: 0x%08" PRIX32, *resistance);
if (this->resistance_ != nullptr) {
this->resistance_->publish_state(*resistance);
}
} else {
ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown");
}
ESP_LOGD(TAG, "Sensor initialized");
}
void AGS10Component::update() {
auto tvoc = this->read_tvoc_();
if (tvoc) {
this->tvoc_->publish_state(*tvoc);
this->status_clear_warning();
} else {
this->status_set_warning();
}
}
void AGS10Component::dump_config() {
ESP_LOGCONFIG(TAG, "AGS10:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) {
case NONE:
break;
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with AGS10 failed!");
break;
case CRC_CHECK_FAILED:
ESP_LOGE(TAG, "The crc check failed");
break;
case ILLEGAL_STATUS:
ESP_LOGE(TAG, "AGS10 is not ready to return TVOC data or sensor in pre-heat stage.");
break;
case UNSUPPORTED_UNITS:
ESP_LOGE(TAG, "AGS10 returns TVOC data in unsupported units.");
break;
default:
ESP_LOGE(TAG, "Unknown error: %d", this->error_code_);
break;
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_);
LOG_SENSOR(" ", "Firmware Version Sensor", this->version_);
LOG_SENSOR(" ", "Resistance Sensor", this->resistance_);
}
/**
* Sets new I2C address of AGS10.
*/
bool AGS10Component::new_i2c_address(uint8_t newaddress) {
uint8_t rev_newaddress = ~newaddress;
std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0};
data[4] = calc_crc8_(data, 4);
if (!this->write_bytes(REG_ADDRESS, data)) {
this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning();
ESP_LOGE(TAG, "couldn't write the new I2C address 0x%02X", newaddress);
return false;
}
this->set_i2c_address(newaddress);
ESP_LOGW(TAG, "changed I2C address to 0x%02X", newaddress);
this->error_code_ = NONE;
this->status_clear_warning();
return true;
}
bool AGS10Component::set_zero_point_with_factory_defaults() { return this->set_zero_point_with(ZP_DEFAULT); }
bool AGS10Component::set_zero_point_with_current_resistance() { return this->set_zero_point_with(ZP_CURRENT); }
bool AGS10Component::set_zero_point_with(uint16_t value) {
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
data[4] = calc_crc8_(data, 4);
if (!this->write_bytes(REG_CALIBRATION, data)) {
this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning();
ESP_LOGE(TAG, "unable to set zero-point calibration with 0x%02X", value);
return false;
}
if (value == ZP_CURRENT) {
ESP_LOGI(TAG, "zero-point calibration has been set with current resistance");
} else if (value == ZP_DEFAULT) {
ESP_LOGI(TAG, "zero-point calibration has been reset to the factory defaults");
} else {
ESP_LOGI(TAG, "zero-point calibration has been set with 0x%02X", value);
}
this->error_code_ = NONE;
this->status_clear_warning();
return true;
}
optional<uint32_t> AGS10Component::read_tvoc_() {
auto data = this->read_and_check_<5>(REG_TVOC);
if (!data) {
return nullopt;
}
auto res = *data;
auto status_byte = res[0];
int units = status_byte & 0x0e;
int status_bit = status_byte & 0x01;
if (status_bit != 0) {
this->error_code_ = ILLEGAL_STATUS;
ESP_LOGW(TAG, "Reading AGS10 data failed: illegal status (not ready or sensor in pre-heat stage)!");
return nullopt;
}
if (units != 0) {
this->error_code_ = UNSUPPORTED_UNITS;
ESP_LOGE(TAG, "Reading AGS10 data failed: unsupported units (%d)!", units);
return nullopt;
}
return encode_uint24(res[1], res[2], res[3]);
}
optional<uint8_t> AGS10Component::read_version_() {
auto data = this->read_and_check_<5>(REG_VERSION);
if (data) {
auto res = *data;
return res[3];
}
return nullopt;
}
optional<uint32_t> AGS10Component::read_resistance_() {
auto data = this->read_and_check_<5>(REG_RESISTANCE);
if (data) {
auto res = *data;
return encode_uint32(res[0], res[1], res[2], res[3]);
}
return nullopt;
}
template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_check_(uint8_t a_register) {
auto data = this->read_bytes<N>(a_register);
if (!data.has_value()) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGE(TAG, "Reading AGS10 version failed!");
return optional<std::array<uint8_t, N>>();
}
auto len = N - 1;
auto res = *data;
auto crc_byte = res[len];
if (crc_byte != calc_crc8_(res, len)) {
this->error_code_ = CRC_CHECK_FAILED;
ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!");
return optional<std::array<uint8_t, N>>();
}
return data;
}
template<size_t N> uint8_t AGS10Component::calc_crc8_(std::array<uint8_t, N> dat, uint8_t num) {
uint8_t i, byte1, crc = 0xFF;
for (byte1 = 0; byte1 < num; byte1++) {
crc ^= (dat[byte1]);
for (i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc = (crc << 1);
}
}
}
return crc;
}
} // namespace ags10
} // namespace esphome

View file

@ -0,0 +1,152 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace ags10 {
class AGS10Component : public PollingComponent, public i2c::I2CDevice {
public:
/**
* Sets TVOC sensor.
*/
void set_tvoc(sensor::Sensor *tvoc) { this->tvoc_ = tvoc; }
/**
* Sets version info sensor.
*/
void set_version(sensor::Sensor *version) { this->version_ = version; }
/**
* Sets resistance info sensor.
*/
void set_resistance(sensor::Sensor *resistance) { this->resistance_ = resistance; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/**
* Modifies target address of AGS10.
*
* New address is saved and takes effect immediately even after power-off.
*/
bool new_i2c_address(uint8_t newaddress);
/**
* Sets zero-point with factory defaults.
*/
bool set_zero_point_with_factory_defaults();
/**
* Sets zero-point with current sensor resistance.
*/
bool set_zero_point_with_current_resistance();
/**
* Sets zero-point with the value.
*/
bool set_zero_point_with(uint16_t value);
protected:
/**
* TVOC.
*/
sensor::Sensor *tvoc_{nullptr};
/**
* Firmvare version.
*/
sensor::Sensor *version_{nullptr};
/**
* Resistance.
*/
sensor::Sensor *resistance_{nullptr};
/**
* Last operation error code.
*/
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
CRC_CHECK_FAILED,
ILLEGAL_STATUS,
UNSUPPORTED_UNITS,
} error_code_{NONE};
/**
* Reads and returns value of TVOC.
*/
optional<uint32_t> read_tvoc_();
/**
* Reads and returns a firmware version of AGS10.
*/
optional<uint8_t> read_version_();
/**
* Reads and returns the resistance of AGS10.
*/
optional<uint32_t> read_resistance_();
/**
* Read, checks and returns data from the sensor.
*/
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
/**
* Calculates CRC8 value.
*
* CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1)
*
* @param[in] dat the data buffer
* @param num number of bytes in the buffer
*/
template<size_t N> uint8_t calc_crc8_(std::array<uint8_t, N> dat, uint8_t num);
};
template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> {
public:
TEMPLATABLE_VALUE(uint8_t, new_address)
void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
};
enum AGS10SetZeroPointActionMode {
// Zero-point reset.
FACTORY_DEFAULT,
// Zero-point calibration with current resistance.
CURRENT_VALUE,
// Zero-point calibration with custom resistance.
CUSTOM_VALUE,
};
template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, public Parented<AGS10Component> {
public:
TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
void play(Ts... x) override {
switch (this->mode_.value(x...)) {
case FACTORY_DEFAULT:
this->parent_->set_zero_point_with_factory_defaults();
break;
case CURRENT_VALUE:
this->parent_->set_zero_point_with_current_resistance();
break;
case CUSTOM_VALUE:
this->parent_->set_zero_point_with(this->value_.value(x...));
break;
}
}
};
} // namespace ags10
} // namespace esphome

View file

@ -0,0 +1,132 @@
import esphome.codegen as cg
from esphome import automation
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
ICON_RADIATOR,
ICON_RESTART,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_OHM,
UNIT_PARTS_PER_BILLION,
CONF_ADDRESS,
CONF_TVOC,
CONF_VERSION,
CONF_MODE,
CONF_VALUE,
)
CONF_RESISTANCE = "resistance"
DEPENDENCIES = ["i2c"]
ags10_ns = cg.esphome_ns.namespace("ags10")
AGS10Component = ags10_ns.class_("AGS10Component", cg.PollingComponent, i2c.I2CDevice)
# Actions
AGS10NewI2cAddressAction = ags10_ns.class_(
"AGS10NewI2cAddressAction", automation.Action
)
AGS10SetZeroPointAction = ags10_ns.class_("AGS10SetZeroPointAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AGS10Component),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VERSION): sensor.sensor_schema(
icon=ICON_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_RESISTANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_OHM,
icon=ICON_RESTART,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x1A))
)
FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz")
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION):
sens = await sensor.new_sensor(version_config)
cg.add(var.set_version(sens))
if resistance_config := config.get(CONF_RESISTANCE):
sens = await sensor.new_sensor(resistance_config)
cg.add(var.set_resistance(sens))
AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(AGS10Component),
cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address),
},
key=CONF_ADDRESS,
)
@automation.register_action(
"ags10.new_i2c_address",
AGS10NewI2cAddressAction,
AGS10_NEW_I2C_ADDRESS_SCHEMA,
)
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
address = await cg.templatable(config[CONF_ADDRESS], args, int)
cg.add(var.set_new_address(address))
return var
AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode")
AGS10_SET_ZERO_POINT_ACTION_MODE = {
"FACTORY_DEFAULT": AGS10SetZeroPointActionMode.FACTORY_DEFAULT,
"CURRENT_VALUE": AGS10SetZeroPointActionMode.CURRENT_VALUE,
"CUSTOM_VALUE": AGS10SetZeroPointActionMode.CUSTOM_VALUE,
}
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(AGS10Component),
cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True),
cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t),
},
)
@automation.register_action(
"ags10.set_zero_point",
AGS10SetZeroPointAction,
AGS10_SET_ZERO_POINT_SCHEMA,
)
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
mode = await cg.templatable(config.get(CONF_MODE), args, enumerate)
cg.add(var.set_mode(mode))
value = await cg.templatable(config[CONF_VALUE], args, int)
cg.add(var.set_value(value))
return var

View file

@ -15,120 +15,145 @@
#include "aht10.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace aht10 {
static const char *const TAG = "aht10";
static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1};
static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement
static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms
static const uint8_t AHT10_SOFTRESET_CMD[] = {0xBA};
static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for initialization and temperature measurement
static const uint8_t AHT10_READ_DELAY = 80; // ms, time to wait for conversion result
static const uint8_t AHT10_SOFTRESET_DELAY = 30; // ms
static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms
static const uint8_t AHT10_INIT_ATTEMPTS = 10;
static const uint8_t AHT10_STATUS_BUSY = 0x80;
void AHT10Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AHT10...");
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Reset AHT10 failed!");
}
delay(AHT10_SOFTRESET_DELAY);
if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) {
i2c::ErrorCode error_code = i2c::ERROR_INVALID_ARGUMENT;
switch (this->variant_) {
case AHT10Variant::AHT20:
ESP_LOGCONFIG(TAG, "Setting up AHT20");
error_code = this->write(AHT20_INITIALIZE_CMD, sizeof(AHT20_INITIALIZE_CMD));
break;
case AHT10Variant::AHT10:
ESP_LOGCONFIG(TAG, "Setting up AHT10");
error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
break;
}
if (error_code != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
uint8_t data = 0;
if (this->write(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
uint8_t data = AHT10_STATUS_BUSY;
int cal_attempts = 0;
while (data & AHT10_STATUS_BUSY) {
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
}
if (this->read(&data, 1) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed!");
++cal_attempts;
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
ESP_LOGE(TAG, "AHT10 initialization timed out!");
this->mark_failed();
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
ESP_LOGE(TAG, "AHT10 calibration failed!");
ESP_LOGE(TAG, "AHT10 initialization failed!");
this->mark_failed();
return;
}
ESP_LOGV(TAG, "AHT10 calibrated");
ESP_LOGV(TAG, "AHT10 initialization");
}
void AHT10Component::update() {
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0;
this->status_set_error("Measurements reading timed-out!");
return;
}
this->read_count_++;
this->set_timeout(AHT10_READ_DELAY, [this]() { this->read_data_(); });
}
void AHT10Component::read_data_() {
uint8_t data[6];
uint8_t delay_ms = AHT10_DEFAULT_DELAY;
if (this->humidity_sensor_ != nullptr)
delay_ms = AHT10_HUMIDITY_DELAY;
bool success = false;
for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
ESP_LOGVV(TAG, "Attempt %d at %6" PRIu32, i, millis());
delay(delay_ms);
if (this->read_count_ > 1) {
ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
}
if (this->read(data, 6) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
continue;
this->status_set_warning("AHT10 read failed, retrying soon");
this->restart_read_();
return;
}
if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy
ESP_LOGD(TAG, "AHT10 is busy, waiting...");
} else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
this->restart_read_();
return;
}
if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
break;
ESP_LOGV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
} else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
this->status_set_warning("Communication with AHT10 failed!");
}
this->restart_read_();
return;
}
}
} else {
// data is valid, we can break the loop
ESP_LOGVV(TAG, "Answer at %6" PRIu32, millis());
success = true;
break;
if (this->read_count_ > 1) {
ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_));
}
}
if (!success || (data[0] & 0x80) == 0x80) {
ESP_LOGE(TAG, "Measurements reading timed-out!");
this->status_set_warning();
return;
}
uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4;
if (this->temperature_sensor_ != nullptr) {
float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
float humidity;
if (raw_humidity == 0) { // unrealistic value
humidity = NAN;
} else {
humidity = (float) raw_humidity * 100.0f / 1048576.0f;
}
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
if (std::isnan(humidity)) {
ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum");
}
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
this->read_count_ = 0;
}
void AHT10Component::update() {
if (this->read_count_ != 0)
return;
this->start_time_ = millis();
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
this->status_set_warning("Communication with AHT10 failed!");
return;
}
this->restart_read_();
}
float AHT10Component::get_setup_priority() const { return setup_priority::DATA; }

View file

@ -1,5 +1,7 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
@ -7,12 +9,15 @@
namespace esphome {
namespace aht10 {
enum AHT10Variant { AHT10, AHT20 };
class AHT10Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override;
void set_variant(AHT10Variant variant) { this->variant_ = variant; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@ -20,6 +25,11 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
unsigned read_count_{};
void read_data_();
void restart_read_();
uint32_t start_time_{};
};
} // namespace aht10

View file

@ -10,6 +10,7 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
CONF_VARIANT,
)
DEPENDENCIES = ["i2c"]
@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"]
aht10_ns = cg.esphome_ns.namespace("aht10")
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
AHT10Variant = aht10_ns.enum("AHT10Variant")
AHT10_VARIANTS = {
"AHT10": AHT10Variant.AHT10,
"AHT20": AHT10Variant.AHT20,
}
CONFIG_SCHEMA = (
cv.Schema(
{
@ -33,6 +40,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum(
AHT10_VARIANTS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
@ -44,6 +54,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_variant(config[CONF_VARIANT]))
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)

View file

@ -0,0 +1,173 @@
#include "aic3204.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace aic3204 {
static const char *const TAG = "aic3204";
#define ERROR_CHECK(err, msg) \
if (!(err)) { \
ESP_LOGE(TAG, msg); \
this->mark_failed(); \
return; \
}
void AIC3204::setup() {
ESP_LOGCONFIG(TAG, "Setting up AIC3204...");
// Set register page to 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
// Initiate SW reset (PLL is powered off as part of reset)
ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed");
// *** Program clock settings ***
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
// for determining these settings.
// Power up NDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed");
// Power up MDAC and set to 2
ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed");
// Program DOSR = 128
ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed");
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed");
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed");
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed");
// Program the DAC processing block to be used - PRB_P1
ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed");
// *** Select Page 1 ***
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed");
// Enable the internal AVDD_LDO:
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed");
// *** Program Analog Blocks ***
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed");
// Enable Master Analog Power Control
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed");
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed");
// *** Set PowerTune Modes ***
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed");
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed");
// Set the REF charging time to 40ms
ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed");
// HP soft stepping settings for optimal pop performance at power up
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed");
// Route Left DAC to HPL
ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed");
// Route Right DAC to HPR
ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed");
// Route Left DAC to LOL
ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed");
// Route Right DAC to LOR
ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed");
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed");
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed");
// Unmute LOL and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed");
// Unmute LOR and set gain to 0dB
ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed");
// Power up HPL and HPR, LOL and LOR drivers
ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed");
// Wait for 2.5 sec for soft stepping to take effect before attempting power-up
this->set_timeout(2500, [this]() {
// *** Power Up DAC ***
// Select Page 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set PAGE_CTRL failed");
// Power up the Left and Right DAC Channels. Route Left data to Left DAC and Right data to Right DAC.
// DAC Vol control soft step 1 step per DAC word clock.
ERROR_CHECK(this->write_byte(AIC3204_DAC_CH_SET1, 0xd4), "Set DAC_CH_SET1 failed");
// Set left and right DAC digital volume control
ERROR_CHECK(this->write_volume_(), "Set volume failed");
// Unmute left and right channels
ERROR_CHECK(this->write_mute_(), "Set mute failed");
});
}
void AIC3204::dump_config() {
ESP_LOGCONFIG(TAG, "AIC3204:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with AIC3204 failed");
}
}
bool AIC3204::set_mute_off() {
this->is_muted_ = false;
return this->write_mute_();
}
bool AIC3204::set_mute_on() {
this->is_muted_ = true;
return this->write_mute_();
}
bool AIC3204::set_auto_mute_mode(uint8_t auto_mute_mode) {
this->auto_mute_mode_ = auto_mute_mode & 0x07;
ESP_LOGVV(TAG, "Setting auto_mute_mode to 0x%.2x", this->auto_mute_mode_);
return this->write_mute_();
}
bool AIC3204::set_volume(float volume) {
this->volume_ = clamp<float>(volume, 0.0, 1.0);
return this->write_volume_();
}
bool AIC3204::is_muted() { return this->is_muted_; }
float AIC3204::volume() { return this->volume_; }
bool AIC3204::write_mute_() {
uint8_t mute_mode_byte = this->auto_mute_mode_ << 4; // auto-mute control is bits 4-6
mute_mode_byte |= this->is_muted_ ? 0x0c : 0x00; // mute bits are 2-3
if (!this->write_byte(AIC3204_PAGE_CTRL, 0x00) || !this->write_byte(AIC3204_DAC_CH_SET2, mute_mode_byte)) {
ESP_LOGE(TAG, "Writing mute modes failed");
return false;
}
return true;
}
bool AIC3204::write_volume_() {
const int8_t dvc_min_byte = -127;
const int8_t dvc_max_byte = 48;
int8_t volume_byte = dvc_min_byte + (this->volume_ * (dvc_max_byte - dvc_min_byte));
volume_byte = clamp<int8_t>(volume_byte, dvc_min_byte, dvc_max_byte);
ESP_LOGVV(TAG, "Setting volume to 0x%.2x", volume_byte & 0xFF);
if ((!this->write_byte(AIC3204_PAGE_CTRL, 0x00)) || (!this->write_byte(AIC3204_DACL_VOL_D, volume_byte)) ||
(!this->write_byte(AIC3204_DACR_VOL_D, volume_byte))) {
ESP_LOGE(TAG, "Writing volume failed");
return false;
}
return true;
}
} // namespace aic3204
} // namespace esphome

View file

@ -0,0 +1,88 @@
#pragma once
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace aic3204 {
// TLV320AIC3204 Register Addresses
// Page 0
static const uint8_t AIC3204_PAGE_CTRL = 0x00; // Register 0 - Page Control
static const uint8_t AIC3204_SW_RST = 0x01; // Register 1 - Software Reset
static const uint8_t AIC3204_CLK_PLL1 = 0x04; // Register 4 - Clock Setting Register 1, Multiplexers
static const uint8_t AIC3204_CLK_PLL2 = 0x05; // Register 5 - Clock Setting Register 2, P and R values
static const uint8_t AIC3204_CLK_PLL3 = 0x06; // Register 6 - Clock Setting Register 3, J values
static const uint8_t AIC3204_NDAC = 0x0B; // Register 11 - NDAC Divider Value
static const uint8_t AIC3204_MDAC = 0x0C; // Register 12 - MDAC Divider Value
static const uint8_t AIC3204_DOSR = 0x0E; // Register 14 - DOSR Divider Value (LS Byte)
static const uint8_t AIC3204_NADC = 0x12; // Register 18 - NADC Divider Value
static const uint8_t AIC3204_MADC = 0x13; // Register 19 - MADC Divider Value
static const uint8_t AIC3204_AOSR = 0x14; // Register 20 - AOSR Divider Value
static const uint8_t AIC3204_CODEC_IF = 0x1B; // Register 27 - CODEC Interface Control
static const uint8_t AIC3204_AUDIO_IF_4 = 0x1F; // Register 31 - Audio Interface Setting Register 4
static const uint8_t AIC3204_AUDIO_IF_5 = 0x20; // Register 32 - Audio Interface Setting Register 5
static const uint8_t AIC3204_SCLK_MFP3 = 0x38; // Register 56 - SCLK/MFP3 Function Control
static const uint8_t AIC3204_DAC_SIG_PROC = 0x3C; // Register 60 - DAC Sig Processing Block Control
static const uint8_t AIC3204_ADC_SIG_PROC = 0x3D; // Register 61 - ADC Sig Processing Block Control
static const uint8_t AIC3204_DAC_CH_SET1 = 0x3F; // Register 63 - DAC Channel Setup 1
static const uint8_t AIC3204_DAC_CH_SET2 = 0x40; // Register 64 - DAC Channel Setup 2
static const uint8_t AIC3204_DACL_VOL_D = 0x41; // Register 65 - DAC Left Digital Vol Control
static const uint8_t AIC3204_DACR_VOL_D = 0x42; // Register 66 - DAC Right Digital Vol Control
static const uint8_t AIC3204_DRC_ENABLE = 0x44;
static const uint8_t AIC3204_ADC_CH_SET = 0x51; // Register 81 - ADC Channel Setup
static const uint8_t AIC3204_ADC_FGA_MUTE = 0x52; // Register 82 - ADC Fine Gain Adjust/Mute
// Page 1
static const uint8_t AIC3204_PWR_CFG = 0x01; // Register 1 - Power Config
static const uint8_t AIC3204_LDO_CTRL = 0x02; // Register 2 - LDO Control
static const uint8_t AIC3204_PLAY_CFG1 = 0x03; // Register 3 - Playback Config 1
static const uint8_t AIC3204_PLAY_CFG2 = 0x04; // Register 4 - Playback Config 2
static const uint8_t AIC3204_OP_PWR_CTRL = 0x09; // Register 9 - Output Driver Power Control
static const uint8_t AIC3204_CM_CTRL = 0x0A; // Register 10 - Common Mode Control
static const uint8_t AIC3204_HPL_ROUTE = 0x0C; // Register 12 - HPL Routing Select
static const uint8_t AIC3204_HPR_ROUTE = 0x0D; // Register 13 - HPR Routing Select
static const uint8_t AIC3204_LOL_ROUTE = 0x0E; // Register 14 - LOL Routing Selection
static const uint8_t AIC3204_LOR_ROUTE = 0x0F; // Register 15 - LOR Routing Selection
static const uint8_t AIC3204_HPL_GAIN = 0x10; // Register 16 - HPL Driver Gain
static const uint8_t AIC3204_HPR_GAIN = 0x11; // Register 17 - HPR Driver Gain
static const uint8_t AIC3204_LOL_DRV_GAIN = 0x12; // Register 18 - LOL Driver Gain Setting
static const uint8_t AIC3204_LOR_DRV_GAIN = 0x13; // Register 19 - LOR Driver Gain Setting
static const uint8_t AIC3204_HP_START = 0x14; // Register 20 - Headphone Driver Startup
static const uint8_t AIC3204_LPGA_P_ROUTE = 0x34; // Register 52 - Left PGA Positive Input Route
static const uint8_t AIC3204_LPGA_N_ROUTE = 0x36; // Register 54 - Left PGA Negative Input Route
static const uint8_t AIC3204_RPGA_P_ROUTE = 0x37; // Register 55 - Right PGA Positive Input Route
static const uint8_t AIC3204_RPGA_N_ROUTE = 0x39; // Register 57 - Right PGA Negative Input Route
static const uint8_t AIC3204_LPGA_VOL = 0x3B; // Register 59 - Left PGA Volume
static const uint8_t AIC3204_RPGA_VOL = 0x3C; // Register 60 - Right PGA Volume
static const uint8_t AIC3204_ADC_PTM = 0x3D; // Register 61 - ADC Power Tune Config
static const uint8_t AIC3204_AN_IN_CHRG = 0x47; // Register 71 - Analog Input Quick Charging Config
static const uint8_t AIC3204_REF_STARTUP = 0x7B; // Register 123 - Reference Power Up Config
class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
bool set_mute_off() override;
bool set_mute_on() override;
bool set_auto_mute_mode(uint8_t auto_mute_mode);
bool set_volume(float volume) override;
bool is_muted() override;
float volume() override;
protected:
bool write_mute_();
bool write_volume_();
uint8_t auto_mute_mode_{0};
float volume_{0};
};
} // namespace aic3204
} // namespace esphome

View file

@ -0,0 +1,52 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["i2c"]
aic3204_ns = cg.esphome_ns.namespace("aic3204")
AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice)
SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AIC3204),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x18))
)
SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(AIC3204),
cv.Required(CONF_MODE): cv.templatable(cv.int_range(max=7, min=0)),
},
key=CONF_MODE,
)
@automation.register_action(
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA
)
async def aic3204_set_volume_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)
template_ = await cg.templatable(config.get(CONF_MODE), args, int)
cg.add(var.set_auto_mute_mode(template_))
return var
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,23 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "aic3204.h"
namespace esphome {
namespace aic3204 {
template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> {
public:
explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {}
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
protected:
AIC3204 *aic3204_;
};
} // namespace aic3204
} // namespace esphome

View file

@ -14,8 +14,6 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
ESP_LOGD(TAG, "version = %d", value->version);
if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
}
@ -43,6 +41,10 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
if (this->illuminance_sensor_ != nullptr) {
this->illuminance_sensor_->publish_state(value->ambientLight);
}
} else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
}
@ -68,6 +70,7 @@ void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
}
AirthingsWavePlus::AirthingsWavePlus() {

View file

@ -22,6 +22,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
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_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
protected:
bool is_valid_radon_value_(uint16_t radon);
@ -32,6 +33,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *illuminance_sensor_{nullptr};
struct WavePlusReadings {
uint8_t version;

View file

@ -12,6 +12,9 @@ from esphome.const import (
CONF_CO2,
UNIT_BECQUEREL_PER_CUBIC_METER,
UNIT_PARTS_PER_MILLION,
CONF_ILLUMINANCE,
UNIT_LUX,
DEVICE_CLASS_ILLUMINANCE,
)
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@ -45,6 +48,12 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
@ -62,3 +71,6 @@ async def to_code(config):
if config_co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(config_co2)
cg.add(var.set_co2(sens))
if config_illuminance := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config_illuminance)
cg.add(var.set_illuminance(sens))

View file

@ -1,17 +1,20 @@
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
import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import (
CONF_CODE,
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_TRIGGER_ID,
CONF_CODE,
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11"]
CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered"
@ -22,6 +25,8 @@ CONF_ON_ARMED_HOME = "on_armed_home"
CONF_ON_ARMED_NIGHT = "on_armed_night"
CONF_ON_ARMED_AWAY = "on_armed_away"
CONF_ON_DISARMED = "on_disarmed"
CONF_ON_CHIME = "on_chime"
CONF_ON_READY = "on_ready"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
@ -53,19 +58,35 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_(
DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template()
)
ChimeTrigger = alarm_control_panel_ns.class_(
"ChimeTrigger", automation.Trigger.template()
)
ReadyTrigger = alarm_control_panel_ns.class_(
"ReadyTrigger", automation.Trigger.template()
)
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition
)
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
@ -111,7 +132,18 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
}
),
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
}
),
cv.Optional(CONF_ON_READY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
}
),
}
)
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
@ -157,6 +189,17 @@ async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CHIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_alarm_control_panel(var, config):
@ -232,6 +275,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
return var
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
@automation.register_condition(
"alarm_control_panel.ready",
AlarmControlPanelCondition,
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
)
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_condition(
"alarm_control_panel.is_armed",
AlarmControlPanelCondition,

View file

@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
auto call = this->make_call();
call.arm_away();

View file

@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** A numeric representation of the supported features as per HomeAssistant
*
*/
@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
};
} // namespace alarm_control_panel

View file

@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> {
}
};
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
}
};
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
}
};
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}

View file

@ -97,9 +97,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t 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: {
if (param->open.status == ESP_GATT_OK) {
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: {

View file

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

View file

@ -0,0 +1,200 @@
// MIT License
//
// Copyright (c) 2023-2024 Rob Tillaart
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include "am2315c.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace am2315c {
static const char *const TAG = "am2315c";
uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) {
uint8_t crc = 0xFF;
while (len--) {
crc ^= *data++;
for (uint8_t i = 0; i < 8; i++) {
if (crc & 0x80) {
crc <<= 1;
crc ^= 0x31;
} else {
crc <<= 1;
}
}
}
return crc;
}
bool AM2315C::reset_register_(uint8_t reg) {
// code based on demo code sent by www.aosong.com
// no further documentation.
// 0x1B returned 18, 0, 4
// 0x1C returned 18, 65, 0
// 0x1E returned 18, 8, 0
// 18 seems to be status register
// other values unknown.
uint8_t data[3];
data[0] = reg;
data[1] = 0;
data[2] = 0;
ESP_LOGD(TAG, "Reset register: 0x%02x", reg);
if (this->write(data, 3) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Write failed!");
this->mark_failed();
return false;
}
delay(5);
if (this->read(data, 3) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
return false;
}
delay(10);
data[0] = 0xB0 | reg;
if (this->write(data, 3) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Write failed!");
this->mark_failed();
return false;
}
delay(5);
return true;
}
bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
uint32_t raw;
raw = (data[1] << 12) | (data[2] << 4) | (data[3] >> 4);
humidity = raw * 9.5367431640625e-5;
raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
temperature = raw * 1.9073486328125e-4 - 50;
return this->crc8_(data, 6) == data[6];
}
void AM2315C::setup() {
ESP_LOGCONFIG(TAG, "Setting up AM2315C...");
// get status
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
return;
}
// reset registers if required, according to the datasheet
// this can be required after power on, although this was
// never required during testing
if ((status & 0x18) != 0x18) {
ESP_LOGD(TAG, "Resetting AM2315C registers");
if (!this->reset_register_(0x1B)) {
this->mark_failed();
return;
}
if (!this->reset_register_(0x1C)) {
this->mark_failed();
return;
}
if (!this->reset_register_(0x1E)) {
this->mark_failed();
return;
}
}
}
void AM2315C::update() {
// request measurement
uint8_t data[3];
data[0] = 0xAC;
data[1] = 0x33;
data[2] = 0x00;
if (this->write(data, 3) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Write failed!");
this->mark_failed();
return;
}
// wait for hw to complete measurement
set_timeout(160, [this]() {
// check status
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
return;
}
if ((status & 0x80) == 0x80) {
ESP_LOGE(TAG, "HW still busy!");
this->mark_failed();
return;
}
// read
uint8_t data[7];
if (this->read(data, 7) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read failed!");
this->mark_failed();
return;
}
// check for all zeros
bool zeros = true;
for (uint8_t i : data) {
zeros = zeros && (i == 0);
}
if (zeros) {
ESP_LOGW(TAG, "Data all zeros!");
this->status_set_warning();
return;
}
// convert
float temperature = 0.0;
float humidity = 0.0;
if (this->convert_(data, humidity, temperature)) {
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
} else {
ESP_LOGW(TAG, "CRC failed!");
this->status_set_warning();
}
});
}
void AM2315C::dump_config() {
ESP_LOGCONFIG(TAG, "AM2315C:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with AM2315C failed!");
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
float AM2315C::get_setup_priority() const { return setup_priority::DATA; }
} // namespace am2315c
} // namespace esphome

View file

@ -0,0 +1,51 @@
// MIT License
//
// Copyright (c) 2023-2024 Rob Tillaart
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace am2315c {
class AM2315C : public PollingComponent, public i2c::I2CDevice {
public:
void dump_config() override;
void update() override;
void setup() override;
float get_setup_priority() const override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
protected:
uint8_t crc8_(uint8_t *data, uint8_t len);
bool convert_(uint8_t *data, float &humidity, float &temperature);
bool reset_register_(uint8_t reg);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace am2315c
} // namespace esphome

View file

@ -0,0 +1,54 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
am2315c_ns = cg.esphome_ns.namespace("am2315c")
AM2315C = am2315c_ns.class_("AM2315C", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AM2315C),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x38))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))

View file

@ -26,7 +26,9 @@ void Am43::setup() {
void Am43::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) {
this->logged_in_ = false;
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {

View file

@ -1,18 +1,27 @@
import logging
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import font
import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY
from esphome.components.image import (
CONF_USE_TRANSPARENCY,
LOCAL_SCHEMA,
SOURCE_LOCAL,
SOURCE_WEB,
WEB_SCHEMA,
)
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_REPEAT,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
@ -43,6 +52,40 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
def validate_cross_dependencies(config):
"""
@ -67,7 +110,7 @@ ANIMATION_SCHEMA = cv.Schema(
cv.All(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): cv.file_,
cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True
@ -124,7 +167,14 @@ async def animation_action_to_code(config, action_id, template_arg, args):
async def to_code(config):
from PIL import Image
path = CORE.relative_config_path(config[CONF_FILE])
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = espImage.compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
try:
image = Image.open(path)
except Exception as e:
@ -136,8 +186,7 @@ async def to_code(config):
new_width_max, new_height_max = config[CONF_RESIZE]
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
else:
if width > 500 or height > 500:
elif width > 500 or height > 500:
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
@ -157,7 +206,7 @@ async def to_code(config):
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})"
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for pix, a in pixels:
if transparent:
@ -180,7 +229,7 @@ async def to_code(config):
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})"
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for pix in pixels:
data[pos] = pix[0]
@ -203,7 +252,7 @@ async def to_code(config):
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})"
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for r, g, b, a in pixels:
if transparent:
@ -222,7 +271,8 @@ async def to_code(config):
pos += 1
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
data = [0 for _ in range(height * width * 2 * frames)]
bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
@ -232,24 +282,20 @@ async def to_code(config):
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})"
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
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 0xFF
pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
width8 = ((width + 7) // 8) * 8
@ -259,6 +305,8 @@ async def to_code(config):
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
else:
has_alpha = False
frame = image.convert("1", dither=Image.Dither.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])

View file

@ -62,7 +62,7 @@ void Animation::set_frame(int frame) {
}
void Animation::update_data_start_() {
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
const uint32_t image_size = this->get_width_stride() * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}

View file

@ -0,0 +1,4 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
CODEOWNERS = ["@aodrenah"]

View file

@ -0,0 +1,151 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#include "apds9306.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace apds9306 {
static const char *const TAG = "apds9306";
enum { // APDS9306 registers
APDS9306_MAIN_CTRL = 0x00,
APDS9306_ALS_MEAS_RATE = 0x04,
APDS9306_ALS_GAIN = 0x05,
APDS9306_PART_ID = 0x06,
APDS9306_MAIN_STATUS = 0x07,
APDS9306_CLEAR_DATA_0 = 0x0A, // LSB
APDS9306_CLEAR_DATA_1 = 0x0B,
APDS9306_CLEAR_DATA_2 = 0x0C, // MSB
APDS9306_ALS_DATA_0 = 0x0D, // LSB
APDS9306_ALS_DATA_1 = 0x0E,
APDS9306_ALS_DATA_2 = 0x0F, // MSB
APDS9306_INT_CFG = 0x19,
APDS9306_INT_PERSISTENCE = 0x1A,
APDS9306_ALS_THRES_UP_0 = 0x21, // LSB
APDS9306_ALS_THRES_UP_1 = 0x22,
APDS9306_ALS_THRES_UP_2 = 0x23, // MSB
APDS9306_ALS_THRES_LOW_0 = 0x24, // LSB
APDS9306_ALS_THRES_LOW_1 = 0x25,
APDS9306_ALS_THRES_LOW_2 = 0x26, // MSB
APDS9306_ALS_THRES_VAR = 0x27
};
#define APDS9306_ERROR_CHECK(func, error) \
if (!(func)) { \
ESP_LOGE(TAG, error); \
this->mark_failed(); \
return; \
}
#define APDS9306_WARNING_CHECK(func, warning) \
if (!(func)) { \
ESP_LOGW(TAG, warning); \
this->status_set_warning(); \
return; \
}
#define APDS9306_WRITE_BYTE(reg, value) \
ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \
if (!this->write_byte(reg, value)) { \
ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \
this->mark_failed(); \
return; \
}
void APDS9306::setup() {
ESP_LOGCONFIG(TAG, "Setting up APDS9306...");
uint8_t id;
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
this->error_code_ = WRONG_ID;
this->mark_failed();
return;
}
// ALS resolution and measurement, see datasheet or init.py for options
uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_MEAS_RATE, als_meas_rate);
// ALS gain, see datasheet or init.py for options
uint8_t als_gain = (this->gain_ & 0x07);
APDS9306_WRITE_BYTE(APDS9306_ALS_GAIN, als_gain);
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Check for data, clear main status
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
}
void APDS9306::dump_config() {
LOG_SENSOR("", "APDS9306", this);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with APDS9306 failed!");
break;
case WRONG_ID:
ESP_LOGE(TAG, "APDS9306 has invalid id!");
break;
default:
ESP_LOGE(TAG, "Setting up APDS9306 registers failed!");
break;
}
}
ESP_LOGCONFIG(TAG, " Gain: %u", AMBIENT_LIGHT_GAIN_VALUES[this->gain_]);
ESP_LOGCONFIG(TAG, " Measurement rate: %u", MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGCONFIG(TAG, " Measurement Resolution/Bit width: %d", MEASUREMENT_BIT_WIDTH_VALUES[this->bit_width_]);
LOG_UPDATE_INTERVAL(this);
}
void APDS9306::update() {
// Check for new data
uint8_t status;
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
this->status_clear_warning();
if (!(status &= 0b00001000)) { // No new data
return;
}
// Set to standby mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x00);
// Clear MAIN STATUS
APDS9306_WARNING_CHECK(this->read_byte(APDS9306_MAIN_STATUS, &status), "Reading MAIN STATUS failed.");
uint8_t als_data[3];
APDS9306_WARNING_CHECK(this->read_bytes(APDS9306_ALS_DATA_0, als_data, 3), "Reading ALS data has failed.");
// Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
uint32_t light_level = 0x00 | encode_uint24(als_data[2], als_data[1], als_data[0]);
float lux = ((float) light_level / AMBIENT_LIGHT_GAIN_VALUES[this->gain_]) *
(100.0f / MEASUREMENT_RATE_VALUES[this->measurement_rate_]);
ESP_LOGD(TAG, "Got illuminance=%.1flx from", lux);
this->publish_state(lux);
}
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,66 @@
// Based on this datasheet:
// https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace apds9306 {
enum MeasurementBitWidth : uint8_t {
MEASUREMENT_BIT_WIDTH_20 = 0,
MEASUREMENT_BIT_WIDTH_19 = 1,
MEASUREMENT_BIT_WIDTH_18 = 2,
MEASUREMENT_BIT_WIDTH_17 = 3,
MEASUREMENT_BIT_WIDTH_16 = 4,
MEASUREMENT_BIT_WIDTH_13 = 5,
};
static const uint8_t MEASUREMENT_BIT_WIDTH_VALUES[] = {20, 19, 18, 17, 16, 13};
enum MeasurementRate : uint8_t {
MEASUREMENT_RATE_25 = 0,
MEASUREMENT_RATE_50 = 1,
MEASUREMENT_RATE_100 = 2,
MEASUREMENT_RATE_200 = 3,
MEASUREMENT_RATE_500 = 4,
MEASUREMENT_RATE_1000 = 5,
MEASUREMENT_RATE_2000 = 6,
};
static const uint16_t MEASUREMENT_RATE_VALUES[] = {25, 50, 100, 200, 500, 1000, 2000};
enum AmbientLightGain : uint8_t {
AMBIENT_LIGHT_GAIN_1 = 0,
AMBIENT_LIGHT_GAIN_3 = 1,
AMBIENT_LIGHT_GAIN_6 = 2,
AMBIENT_LIGHT_GAIN_9 = 3,
AMBIENT_LIGHT_GAIN_18 = 4,
};
static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18};
class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void dump_config() override;
void update() override;
void set_bit_width(MeasurementBitWidth bit_width) { this->bit_width_ = bit_width; }
void set_measurement_rate(MeasurementRate measurement_rate) { this->measurement_rate_ = measurement_rate; }
void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; }
protected:
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_ID,
} error_code_{NONE};
MeasurementBitWidth bit_width_;
MeasurementRate measurement_rate_;
AmbientLightGain gain_;
};
} // namespace apds9306
} // namespace esphome

View file

@ -0,0 +1,95 @@
# Based on this datasheet:
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_GAIN,
DEVICE_CLASS_ILLUMINANCE,
ICON_LIGHTBULB,
STATE_CLASS_MEASUREMENT,
UNIT_LUX,
)
DEPENDENCIES = ["i2c"]
CONF_APDS9306_ID = "apds9306_id"
CONF_BIT_WIDTH = "bit_width"
CONF_MEASUREMENT_RATE = "measurement_rate"
apds9306_ns = cg.esphome_ns.namespace("apds9306")
APDS9306 = apds9306_ns.class_(
"APDS9306", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
MeasurementBitWidth = apds9306_ns.enum("MeasurementBitWidth")
MeasurementRate = apds9306_ns.enum("MeasurementRate")
AmbientLightGain = apds9306_ns.enum("AmbientLightGain")
MEASUREMENT_BIT_WIDTHS = {
20: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_20,
19: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_19,
18: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_18,
17: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_17,
16: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_16,
13: MeasurementBitWidth.MEASUREMENT_BIT_WIDTH_13,
}
MEASUREMENT_RATES = {
25: MeasurementRate.MEASUREMENT_RATE_25,
50: MeasurementRate.MEASUREMENT_RATE_50,
100: MeasurementRate.MEASUREMENT_RATE_100,
200: MeasurementRate.MEASUREMENT_RATE_200,
500: MeasurementRate.MEASUREMENT_RATE_500,
1000: MeasurementRate.MEASUREMENT_RATE_1000,
2000: MeasurementRate.MEASUREMENT_RATE_2000,
}
AMBIENT_LIGHT_GAINS = {
1: AmbientLightGain.AMBIENT_LIGHT_GAIN_1,
3: AmbientLightGain.AMBIENT_LIGHT_GAIN_3,
6: AmbientLightGain.AMBIENT_LIGHT_GAIN_6,
9: AmbientLightGain.AMBIENT_LIGHT_GAIN_9,
18: AmbientLightGain.AMBIENT_LIGHT_GAIN_18,
}
def _validate_measurement_rate(value):
value = cv.positive_time_period_milliseconds(value)
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
CONFIG_SCHEMA = (
sensor.sensor_schema(
APDS9306,
unit_of_measurement=UNIT_LUX,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_LIGHTBULB,
)
.extend(
{
cv.Optional(CONF_GAIN, default="1"): cv.enum(AMBIENT_LIGHT_GAINS, int=True),
cv.Optional(CONF_BIT_WIDTH, default="18"): cv.enum(
MEASUREMENT_BIT_WIDTHS, int=True
),
cv.Optional(
CONF_MEASUREMENT_RATE, default="100ms"
): _validate_measurement_rate,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x52))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_bit_width(config[CONF_BIT_WIDTH]))
cg.add(var.set_measurement_rate(config[CONF_MEASUREMENT_RATE]))
cg.add(var.set_ambient_light_gain(config[CONF_GAIN]))

View file

@ -1,25 +1,27 @@
import base64
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
CONF_ACTIONS,
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_EVENT,
CONF_ID,
CONF_KEY,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
CONF_SERVICE,
CONF_VARIABLES,
CONF_SERVICES,
CONF_TRIGGER_ID,
CONF_EVENT,
CONF_TAG,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import coroutine_with_priority
@ -63,7 +65,25 @@ def validate_encryption_key(value):
return value
CONFIG_SCHEMA = cv.Schema(
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
},
cv.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
),
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
@ -71,19 +91,10 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVICES): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Required(CONF_SERVICE): cv.valid_name,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: cv.one_of(
*SERVICE_ARG_NATIVE_TYPES, lower=True
),
}
),
}
),
cv.Exclusive(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): cv.Schema(
{
cv.Required(CONF_KEY): validate_encryption_key,
@ -96,7 +107,9 @@ CONFIG_SCHEMA = cv.Schema(
single=True
),
}
).extend(cv.COMPONENT_SCHEMA)
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
)
@coroutine_with_priority(40.0)
@ -108,7 +121,7 @@ async def to_code(config):
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
for conf in config.get(CONF_SERVICES, []):
for conf in config.get(CONF_ACTIONS, []):
template_args = []
func_args = []
service_arg_names = []
@ -119,7 +132,7 @@ async def to_code(config):
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
@ -142,7 +155,7 @@ async def to_code(config):
decoded = base64.b64decode(encryption_config[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.4")
cg.add_library("esphome/noise-c", "0.1.6")
else:
cg.add_define("USE_API_PLAINTEXT")
@ -152,28 +165,43 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema(
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Required(CONF_SERVICE): cv.templatable(cv.string),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable(
cv.string
),
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable(
cv.string
),
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda}
),
}
),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
)
@automation.register_action(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_SERVICE_ACTION_SCHEMA,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
async def homeassistant_service_to_code(config, action_id, template_arg, args):
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_SERVICE], args, None)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)

View file

@ -43,7 +43,12 @@ service APIConnection {
rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc valve_command (ValveCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
rpc date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@ -57,6 +62,8 @@ service APIConnection {
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {}
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
}
@ -216,7 +223,8 @@ message DeviceInfoResponse {
string friendly_name = 13;
uint32 voice_assistant_version = 14;
uint32 legacy_voice_assistant_version = 14;
uint32 voice_assistant_feature_flags = 17;
string suggested_area = 16;
}
@ -365,6 +373,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9;
string icon = 10;
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@ -387,6 +396,7 @@ message FanStateResponse {
FanSpeed speed = 4 [deprecated = true];
FanDirection direction = 5;
int32 speed_level = 6;
string preset_mode = 7;
}
message FanCommandRequest {
option (id) = 31;
@ -405,6 +415,8 @@ message FanCommandRequest {
FanDirection direction = 9;
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
}
// ==================== LIGHT ====================
@ -596,6 +608,7 @@ message ListEntitiesTextSensorResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
}
message TextSensorStateResponse {
option (id) = 27;
@ -675,6 +688,7 @@ message SubscribeHomeAssistantStateResponse {
option (source) = SOURCE_SERVER;
string entity_id = 1;
string attribute = 2;
bool once = 3;
}
message HomeAssistantStateResponse {
@ -855,6 +869,10 @@ message ListEntitiesClimateResponse {
string icon = 19;
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
}
message ClimateStateResponse {
option (id) = 47;
@ -875,6 +893,8 @@ message ClimateStateResponse {
string custom_fan_mode = 11;
ClimatePreset preset = 12;
string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
}
message ClimateCommandRequest {
option (id) = 48;
@ -903,6 +923,8 @@ message ClimateCommandRequest {
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
}
// ==================== NUMBER ====================
@ -1087,6 +1109,19 @@ enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
}
enum MediaPlayerFormatPurpose {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1;
}
message MediaPlayerSupportedFormat {
option (ifdef) = "USE_MEDIA_PLAYER";
string format = 1;
uint32 sample_rate = 2;
uint32 num_channels = 3;
MediaPlayerFormatPurpose purpose = 4;
uint32 sample_bytes = 5;
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
option (source) = SOURCE_SERVER;
@ -1102,6 +1137,8 @@ message ListEntitiesMediaPlayerResponse {
EntityCategory entity_category = 7;
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
}
message MediaPlayerStateResponse {
option (id) = 64;
@ -1129,6 +1166,9 @@ message MediaPlayerCommandRequest {
bool has_media_url = 6;
string media_url = 7;
bool has_announcement = 8;
bool announcement = 9;
}
// ==================== BLUETOOTH ====================
@ -1408,12 +1448,18 @@ message BluetoothDeviceClearCacheResponse {
}
// ==================== PUSH TO TALK ====================
enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
}
message SubscribeVoiceAssistantRequest {
option (id) = 89;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
bool subscribe = 1;
uint32 flags = 2;
}
enum VoiceAssistantRequestFlag {
@ -1437,6 +1483,7 @@ message VoiceAssistantRequest {
string conversation_id = 2;
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
}
message VoiceAssistantResponse {
@ -1480,6 +1527,82 @@ message VoiceAssistantEventResponse {
repeated VoiceAssistantEventData data = 2;
}
message VoiceAssistantAudio {
option (id) = 106;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_VOICE_ASSISTANT";
bytes data = 1;
bool end = 2;
}
enum VoiceAssistantTimerEvent {
VOICE_ASSISTANT_TIMER_STARTED = 0;
VOICE_ASSISTANT_TIMER_UPDATED = 1;
VOICE_ASSISTANT_TIMER_CANCELLED = 2;
VOICE_ASSISTANT_TIMER_FINISHED = 3;
}
message VoiceAssistantTimerEventResponse {
option (id) = 115;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
VoiceAssistantTimerEvent event_type = 1;
string timer_id = 2;
string name = 3;
uint32 total_seconds = 4;
uint32 seconds_left = 5;
bool is_active = 6;
}
message VoiceAssistantAnnounceRequest {
option (id) = 119;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
string media_id = 1;
string text = 2;
}
message VoiceAssistantAnnounceFinished {
option (id) = 120;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
bool success = 1;
}
message VoiceAssistantWakeWord {
string id = 1;
string wake_word = 2;
repeated string trained_languages = 3;
}
message VoiceAssistantConfigurationRequest {
option (id) = 121;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
}
message VoiceAssistantConfigurationResponse {
option (id) = 122;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantWakeWord available_wake_words = 1;
repeated string active_wake_words = 2;
uint32 max_active_wake_words = 3;
}
message VoiceAssistantSetConfiguration {
option (id) = 123;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated string active_wake_words = 1;
}
// ==================== ALARM CONTROL PANEL ====================
enum AlarmControlPanelState {
ALARM_STATE_DISARMED = 0;
@ -1584,3 +1707,247 @@ message TextCommandRequest {
fixed32 key = 1;
string state = 2;
}
// ==================== DATETIME DATE ====================
message ListEntitiesDateResponse {
option (id) = 100;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
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;
}
message DateStateResponse {
option (id) = 101;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
fixed32 key = 1;
// If the date does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
uint32 year = 3;
uint32 month = 4;
uint32 day = 5;
}
message DateCommandRequest {
option (id) = 102;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
fixed32 key = 1;
uint32 year = 2;
uint32 month = 3;
uint32 day = 4;
}
// ==================== DATETIME TIME ====================
message ListEntitiesTimeResponse {
option (id) = 103;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
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;
}
message TimeStateResponse {
option (id) = 104;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
fixed32 key = 1;
// If the time does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
uint32 hour = 3;
uint32 minute = 4;
uint32 second = 5;
}
message TimeCommandRequest {
option (id) = 105;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
fixed32 key = 1;
uint32 hour = 2;
uint32 minute = 3;
uint32 second = 4;
}
// ==================== EVENT ====================
message ListEntitiesEventResponse {
option (id) = 107;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
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;
string device_class = 8;
repeated string event_types = 9;
}
message EventResponse {
option (id) = 108;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
fixed32 key = 1;
string event_type = 2;
}
// ==================== VALVE ====================
message ListEntitiesValveResponse {
option (id) = 109;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
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;
string device_class = 8;
bool assumed_state = 9;
bool supports_position = 10;
bool supports_stop = 11;
}
enum ValveOperation {
VALVE_OPERATION_IDLE = 0;
VALVE_OPERATION_IS_OPENING = 1;
VALVE_OPERATION_IS_CLOSING = 2;
}
message ValveStateResponse {
option (id) = 110;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
fixed32 key = 1;
float position = 2;
ValveOperation current_operation = 3;
}
message ValveCommandRequest {
option (id) = 111;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
fixed32 key = 1;
bool has_position = 2;
float position = 3;
bool stop = 4;
}
// ==================== DATETIME DATETIME ====================
message ListEntitiesDateTimeResponse {
option (id) = 112;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
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;
}
message DateTimeStateResponse {
option (id) = 113;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
fixed32 key = 1;
// If the datetime does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
fixed32 epoch_seconds = 3;
}
message DateTimeCommandRequest {
option (id) = 114;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
fixed32 key = 1;
fixed32 epoch_seconds = 2;
}
// ==================== UPDATE ====================
message ListEntitiesUpdateResponse {
option (id) = 116;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
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;
string device_class = 8;
}
message UpdateStateResponse {
option (id) = 117;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
fixed32 key = 1;
bool missing_state = 2;
bool in_progress = 3;
bool has_progress = 4;
float progress = 5;
string current_version = 6;
string latest_version = 7;
string title = 8;
string release_summary = 9;
string release_url = 10;
}
enum UpdateCommand {
UPDATE_COMMAND_NONE = 0;
UPDATE_COMMAND_UPDATE = 1;
UPDATE_COMMAND_CHECK = 2;
}
message UpdateCommandRequest {
option (id) = 118;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
fixed32 key = 1;
UpdateCommand command = 2;
}

View file

@ -1,4 +1,5 @@
#include "api_connection.h"
#ifdef USE_API
#include <cerrno>
#include <cinttypes>
#include <utility>
@ -118,7 +119,9 @@ void APIConnection::loop() {
this->list_entities_iterator_.advance();
this->initial_state_iterator_.advance();
const uint32_t keepalive = 60000;
static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = millis();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
@ -126,10 +129,24 @@ void APIConnection::loop() {
on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
}
} else if (now - this->last_traffic_ > keepalive) {
} else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = true;
this->send_ping_request(PingRequest());
this->sent_ping_ = this->send_ping_request(PingRequest());
if (!this->sent_ping_) {
this->next_ping_retry_ = now + ping_retry_interval;
this->ping_retries_++;
if (this->ping_retries_ >= max_ping_retries) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(),
this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
} else {
ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
}
}
}
#ifdef USE_ESP32_CAMERA
@ -163,6 +180,7 @@ void APIConnection::loop() {
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value();
resp.once = it.once;
if (this->send_subscribe_home_assistant_state_response(resp)) {
state_subs_at_++;
}
@ -293,6 +311,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
}
if (traits.supports_direction())
resp.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes())
resp.preset_mode = fan->preset_mode;
return this->send_fan_state_response(resp);
}
bool APIConnection::send_fan_info(fan::Fan *fan) {
@ -307,6 +327,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) {
msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count();
for (auto const &preset : traits.supported_preset_modes())
msg.supported_preset_modes.push_back(preset);
msg.disabled_by_default = fan->is_disabled_by_default();
msg.icon = fan->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category());
@ -328,6 +350,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
}
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.perform();
}
#endif
@ -521,6 +545,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor)
msg.icon = text_sensor->get_icon();
msg.disabled_by_default = text_sensor->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category());
msg.device_class = text_sensor->get_device_class();
return this->send_list_entities_text_sensor_response(msg);
}
#endif
@ -554,6 +579,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.custom_preset = climate->custom_preset.value();
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity;
return this->send_climate_state_response(resp);
}
bool APIConnection::send_climate_info(climate::Climate *climate) {
@ -570,7 +599,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category());
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
for (auto mode : traits.get_supported_modes())
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
@ -579,6 +610,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action();
@ -609,6 +642,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_target_temperature_high)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_target_humidity)
call.set_target_humidity(msg.target_humidity);
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)
@ -665,6 +700,118 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
}
#endif
#ifdef USE_DATETIME_DATE
bool APIConnection::send_date_state(datetime::DateEntity *date) {
if (!this->state_subscription_)
return false;
DateStateResponse resp{};
resp.key = date->get_object_id_hash();
resp.missing_state = !date->has_state();
resp.year = date->year;
resp.month = date->month;
resp.day = date->day;
return this->send_date_state_response(resp);
}
bool APIConnection::send_date_info(datetime::DateEntity *date) {
ListEntitiesDateResponse msg;
msg.key = date->get_object_id_hash();
msg.object_id = date->get_object_id();
if (date->has_own_name())
msg.name = date->get_name();
msg.unique_id = get_default_unique_id("date", date);
msg.icon = date->get_icon();
msg.disabled_by_default = date->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(date->get_entity_category());
return this->send_list_entities_date_response(msg);
}
void APIConnection::date_command(const DateCommandRequest &msg) {
datetime::DateEntity *date = App.get_date_by_key(msg.key);
if (date == nullptr)
return;
auto call = date->make_call();
call.set_date(msg.year, msg.month, msg.day);
call.perform();
}
#endif
#ifdef USE_DATETIME_TIME
bool APIConnection::send_time_state(datetime::TimeEntity *time) {
if (!this->state_subscription_)
return false;
TimeStateResponse resp{};
resp.key = time->get_object_id_hash();
resp.missing_state = !time->has_state();
resp.hour = time->hour;
resp.minute = time->minute;
resp.second = time->second;
return this->send_time_state_response(resp);
}
bool APIConnection::send_time_info(datetime::TimeEntity *time) {
ListEntitiesTimeResponse msg;
msg.key = time->get_object_id_hash();
msg.object_id = time->get_object_id();
if (time->has_own_name())
msg.name = time->get_name();
msg.unique_id = get_default_unique_id("time", time);
msg.icon = time->get_icon();
msg.disabled_by_default = time->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(time->get_entity_category());
return this->send_list_entities_time_response(msg);
}
void APIConnection::time_command(const TimeCommandRequest &msg) {
datetime::TimeEntity *time = App.get_time_by_key(msg.key);
if (time == nullptr)
return;
auto call = time->make_call();
call.set_time(msg.hour, msg.minute, msg.second);
call.perform();
}
#endif
#ifdef USE_DATETIME_DATETIME
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
if (!this->state_subscription_)
return false;
DateTimeStateResponse resp{};
resp.key = datetime->get_object_id_hash();
resp.missing_state = !datetime->has_state();
if (datetime->has_state()) {
ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp;
}
return this->send_date_time_state_response(resp);
}
bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
ListEntitiesDateTimeResponse msg;
msg.key = datetime->get_object_id_hash();
msg.object_id = datetime->get_object_id();
if (datetime->has_own_name())
msg.name = datetime->get_name();
msg.unique_id = get_default_unique_id("datetime", datetime);
msg.icon = datetime->get_icon();
msg.disabled_by_default = datetime->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(datetime->get_entity_category());
return this->send_list_entities_date_time_response(msg);
}
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
if (datetime == nullptr)
return;
auto call = datetime->make_call();
call.set_datetime(msg.epoch_seconds);
call.perform();
}
#endif
#ifdef USE_TEXT
bool APIConnection::send_text_state(text::Text *text, std::string state) {
if (!this->state_subscription_)
@ -808,6 +955,48 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
}
#endif
#ifdef USE_VALVE
bool APIConnection::send_valve_state(valve::Valve *valve) {
if (!this->state_subscription_)
return false;
ValveStateResponse resp{};
resp.key = valve->get_object_id_hash();
resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
return this->send_valve_state_response(resp);
}
bool APIConnection::send_valve_info(valve::Valve *valve) {
auto traits = valve->get_traits();
ListEntitiesValveResponse msg;
msg.key = valve->get_object_id_hash();
msg.object_id = valve->get_object_id();
if (valve->has_own_name())
msg.name = valve->get_name();
msg.unique_id = get_default_unique_id("valve", valve);
msg.icon = valve->get_icon();
msg.disabled_by_default = valve->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(valve->get_entity_category());
msg.device_class = valve->get_device_class();
msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position();
msg.supports_stop = traits.get_supports_stop();
return this->send_list_entities_valve_response(msg);
}
void APIConnection::valve_command(const ValveCommandRequest &msg) {
valve::Valve *valve = App.get_valve_by_key(msg.key);
if (valve == nullptr)
return;
auto call = valve->make_call();
if (msg.has_position)
call.set_position(msg.position);
if (msg.stop)
call.set_command_stop();
call.perform();
}
#endif
#ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
if (!this->state_subscription_)
@ -815,7 +1004,11 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla
MediaPlayerStateResponse resp{};
resp.key = media_player->get_object_id_hash();
resp.state = static_cast<enums::MediaPlayerState>(media_player->state);
media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING
? media_player::MEDIA_PLAYER_STATE_PLAYING
: media_player->state;
resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume;
resp.muted = media_player->is_muted();
return this->send_media_player_state_response(resp);
@ -834,6 +1027,16 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause();
for (auto &supported_format : traits.get_supported_formats()) {
MediaPlayerSupportedFormat media_format;
media_format.format = supported_format.format;
media_format.sample_rate = supported_format.sample_rate;
media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format);
}
return this->send_list_entities_media_player_response(msg);
}
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
@ -851,6 +1054,9 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
if (msg.has_media_url) {
call.set_media_url(msg.media_url);
}
if (msg.has_announcement) {
call.set_announcement(msg.announcement);
}
call.perform();
}
#endif
@ -970,11 +1176,16 @@ void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &ms
voice_assistant::global_voice_assistant->failed_to_start();
return;
}
if (msg.port == 0) {
// Use API Audio
voice_assistant::global_voice_assistant->start_streaming();
} else {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
this->helper_->getpeername((struct sockaddr *) &storage, &len);
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
}
}
};
void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
@ -985,6 +1196,70 @@ void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventR
voice_assistant::global_voice_assistant->on_event(msg);
}
}
void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_audio(msg);
}
};
void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_timer_event(msg);
}
};
void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_announce(msg);
}
}
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return resp;
}
auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word;
resp_wake_word.id = wake_word.id;
resp_wake_word.wake_word = wake_word.wake_word;
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
resp.available_wake_words.push_back(std::move(resp_wake_word));
}
for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words;
}
return resp;
}
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
return;
}
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
}
#endif
@ -1046,6 +1321,88 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
}
#endif
#ifdef USE_EVENT
bool APIConnection::send_event(event::Event *event, std::string event_type) {
EventResponse resp{};
resp.key = event->get_object_id_hash();
resp.event_type = std::move(event_type);
return this->send_event_response(resp);
}
bool APIConnection::send_event_info(event::Event *event) {
ListEntitiesEventResponse msg;
msg.key = event->get_object_id_hash();
msg.object_id = event->get_object_id();
if (event->has_own_name())
msg.name = event->get_name();
msg.unique_id = get_default_unique_id("event", event);
msg.icon = event->get_icon();
msg.disabled_by_default = event->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(event->get_entity_category());
msg.device_class = event->get_device_class();
for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type);
return this->send_list_entities_event_response(msg);
}
#endif
#ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) {
if (!this->state_subscription_)
return false;
UpdateStateResponse resp{};
resp.key = update->get_object_id_hash();
resp.missing_state = !update->has_state();
if (update->has_state()) {
resp.in_progress = update->state == update::UpdateState::UPDATE_STATE_INSTALLING;
if (update->update_info.has_progress) {
resp.has_progress = true;
resp.progress = update->update_info.progress;
}
resp.current_version = update->update_info.current_version;
resp.latest_version = update->update_info.latest_version;
resp.title = update->update_info.title;
resp.release_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url;
}
return this->send_update_state_response(resp);
}
bool APIConnection::send_update_info(update::UpdateEntity *update) {
ListEntitiesUpdateResponse msg;
msg.key = update->get_object_id_hash();
msg.object_id = update->get_object_id();
if (update->has_own_name())
msg.name = update->get_name();
msg.unique_id = get_default_unique_id("update", update);
msg.icon = update->get_icon();
msg.disabled_by_default = update->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(update->get_entity_category());
msg.device_class = update->get_device_class();
return this->send_list_entities_update_response(msg);
}
void APIConnection::update_command(const UpdateCommandRequest &msg) {
update::UpdateEntity *update = App.get_update_by_key(msg.key);
if (update == nullptr)
return;
switch (msg.command) {
case enums::UPDATE_COMMAND_UPDATE:
update->perform();
break;
case enums::UPDATE_COMMAND_CHECK:
update->check();
break;
case enums::UPDATE_COMMAND_NONE:
ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command");
break;
default:
ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command);
break;
}
}
#endif
bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
if (this->log_subscription_ < level)
return false;
@ -1072,7 +1429,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 9;
resp.api_version_minor = 10;
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.name = App.get_name();
@ -1133,7 +1490,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
#endif
#ifdef USE_VOICE_ASSISTANT
resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version();
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
#endif
return resp;
}
@ -1211,3 +1569,4 @@ void APIConnection::on_fatal_error() {
} // namespace api
} // namespace esphome
#endif

View file

@ -1,12 +1,13 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_frame_helper.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include <vector>
@ -72,6 +73,21 @@ class APIConnection : public APIServerConnection {
bool send_number_info(number::Number *number);
void number_command(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
bool send_date_info(datetime::DateEntity *date);
void date_command(const DateCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
bool send_time_info(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
bool send_datetime_info(datetime::DateTimeEntity *datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state);
bool send_text_info(text::Text *text);
@ -91,6 +107,11 @@ class APIConnection : public APIServerConnection {
bool send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
bool send_valve_info(valve::Valve *valve);
void valve_command(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
bool send_media_player_info(media_player::MediaPlayer *media_player);
@ -129,6 +150,12 @@ class APIConnection : public APIServerConnection {
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &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_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
@ -137,9 +164,21 @@ class APIConnection : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
bool send_event(event::Event *event, std::string event_type);
bool send_event_info(event::Event *event);
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
bool send_update_info(update::UpdateEntity *update);
void update_command(const UpdateCommandRequest &msg) override;
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false;
}
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@ -217,6 +256,8 @@ class APIConnection : public APIServerConnection {
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
@ -228,3 +269,4 @@ class APIConnection : public APIServerConnection {
} // namespace api
} // namespace esphome
#endif

View file

@ -1,5 +1,5 @@
#include "api_frame_helper.h"
#ifdef USE_API
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@ -1028,3 +1028,4 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
} // namespace api
} // namespace esphome
#endif

View file

@ -5,7 +5,7 @@
#include <vector>
#include "esphome/core/defines.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#endif
@ -190,3 +190,4 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
} // namespace api
} // namespace esphome
#endif

File diff suppressed because it is too large Load diff

View file

@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
};
enum MediaPlayerFormatPurpose : uint32_t {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1,
};
enum BluetoothDeviceRequestType : uint32_t {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0,
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1,
@ -165,6 +169,10 @@ enum BluetoothDeviceRequestType : uint32_t {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5,
BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6,
};
enum VoiceAssistantSubscribeFlag : uint32_t {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
};
enum VoiceAssistantRequestFlag : uint32_t {
VOICE_ASSISTANT_REQUEST_NONE = 0,
VOICE_ASSISTANT_REQUEST_USE_VAD = 1,
@ -187,6 +195,12 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99,
};
enum VoiceAssistantTimerEvent : uint32_t {
VOICE_ASSISTANT_TIMER_STARTED = 0,
VOICE_ASSISTANT_TIMER_UPDATED = 1,
VOICE_ASSISTANT_TIMER_CANCELLED = 2,
VOICE_ASSISTANT_TIMER_FINISHED = 3,
};
enum AlarmControlPanelState : uint32_t {
ALARM_STATE_DISARMED = 0,
ALARM_STATE_ARMED_HOME = 1,
@ -212,6 +226,16 @@ enum TextMode : uint32_t {
TEXT_MODE_TEXT = 0,
TEXT_MODE_PASSWORD = 1,
};
enum ValveOperation : uint32_t {
VALVE_OPERATION_IDLE = 0,
VALVE_OPERATION_IS_OPENING = 1,
VALVE_OPERATION_IS_CLOSING = 2,
};
enum UpdateCommand : uint32_t {
UPDATE_COMMAND_NONE = 0,
UPDATE_COMMAND_UPDATE = 1,
UPDATE_COMMAND_CHECK = 2,
};
} // namespace enums
@ -327,7 +351,8 @@ class DeviceInfoResponse : public ProtoMessage {
uint32_t bluetooth_proxy_feature_flags{0};
std::string manufacturer{};
std::string friendly_name{};
uint32_t voice_assistant_version{0};
uint32_t legacy_voice_assistant_version{0};
uint32_t voice_assistant_feature_flags{0};
std::string suggested_area{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@ -472,6 +497,7 @@ class ListEntitiesFanResponse : public ProtoMessage {
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
std::vector<std::string> supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -490,6 +516,7 @@ class FanStateResponse : public ProtoMessage {
enums::FanSpeed speed{};
enums::FanDirection direction{};
int32_t speed_level{0};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -497,6 +524,7 @@ class FanStateResponse : public ProtoMessage {
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 FanCommandRequest : public ProtoMessage {
@ -512,6 +540,8 @@ class FanCommandRequest : public ProtoMessage {
enums::FanDirection direction{};
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -519,6 +549,7 @@ class FanCommandRequest : public ProtoMessage {
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 ListEntitiesLightResponse : public ProtoMessage {
@ -707,6 +738,7 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -808,6 +840,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
public:
std::string entity_id{};
std::string attribute{};
bool once{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -815,6 +848,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class HomeAssistantStateResponse : public ProtoMessage {
public:
@ -979,6 +1013,10 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::string icon{};
enums::EntityCategory entity_category{};
float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1004,6 +1042,8 @@ class ClimateStateResponse : public ProtoMessage {
std::string custom_fan_mode{};
enums::ClimatePreset preset{};
std::string custom_preset{};
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1037,6 +1077,8 @@ class ClimateCommandRequest : public ProtoMessage {
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
bool has_target_humidity{false};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1229,6 +1271,22 @@ class ButtonCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class MediaPlayerSupportedFormat : public ProtoMessage {
public:
std::string format{};
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
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 ListEntitiesMediaPlayerResponse : public ProtoMessage {
public:
std::string object_id{};
@ -1239,6 +1297,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage {
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1273,6 +1332,8 @@ class MediaPlayerCommandRequest : public ProtoMessage {
float volume{0.0f};
bool has_media_url{false};
std::string media_url{};
bool has_announcement{false};
bool announcement{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1659,6 +1720,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage {
class SubscribeVoiceAssistantRequest : public ProtoMessage {
public:
bool subscribe{false};
uint32_t flags{0};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1687,6 +1749,7 @@ class VoiceAssistantRequest : public ProtoMessage {
std::string conversation_id{};
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
std::string wake_word_phrase{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1733,6 +1796,106 @@ class VoiceAssistantEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantAudio : public ProtoMessage {
public:
std::string data{};
bool end{false};
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 VoiceAssistantTimerEventResponse : public ProtoMessage {
public:
enums::VoiceAssistantTimerEvent event_type{};
std::string timer_id{};
std::string name{};
uint32_t total_seconds{0};
uint32_t seconds_left{0};
bool is_active{false};
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 VoiceAssistantAnnounceRequest : public ProtoMessage {
public:
std::string media_id{};
std::string text{};
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 VoiceAssistantAnnounceFinished : public ProtoMessage {
public:
bool success{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantWakeWord : public ProtoMessage {
public:
std::string id{};
std::string wake_word{};
std::vector<std::string> trained_languages{};
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 VoiceAssistantConfigurationRequest : public ProtoMessage {
public:
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class VoiceAssistantConfigurationResponse : public ProtoMessage {
public:
std::vector<VoiceAssistantWakeWord> available_wake_words{};
std::vector<std::string> active_wake_words{};
uint32_t max_active_wake_words{0};
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 VoiceAssistantSetConfiguration : public ProtoMessage {
public:
std::vector<std::string> active_wake_words{};
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 ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
public:
std::string object_id{};
@ -1834,6 +1997,292 @@ class TextCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesDateResponse : 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{};
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 DateStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
uint32_t year{0};
uint32_t month{0};
uint32_t day{0};
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 DateCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
uint32_t year{0};
uint32_t month{0};
uint32_t day{0};
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 ListEntitiesTimeResponse : 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{};
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 TimeStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{0};
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 TimeCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{0};
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 ListEntitiesEventResponse : 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{};
std::string device_class{};
std::vector<std::string> event_types{};
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 EventResponse : public ProtoMessage {
public:
uint32_t key{0};
std::string event_type{};
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;
};
class ListEntitiesValveResponse : 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{};
std::string device_class{};
bool assumed_state{false};
bool supports_position{false};
bool supports_stop{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 ValveStateResponse : public ProtoMessage {
public:
uint32_t key{0};
float position{0.0f};
enums::ValveOperation current_operation{};
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 ValveCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
bool has_position{false};
float position{0.0f};
bool stop{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_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesDateTimeResponse : 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{};
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 DateTimeStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
uint32_t epoch_seconds{0};
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 DateTimeCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
uint32_t epoch_seconds{0};
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;
};
class ListEntitiesUpdateResponse : 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{};
std::string device_class{};
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 UpdateStateResponse : public ProtoMessage {
public:
uint32_t key{0};
bool missing_state{false};
bool in_progress{false};
bool has_progress{false};
float progress{0.0f};
std::string current_version{};
std::string latest_version{};
std::string title{};
std::string release_summary{};
std::string release_url{};
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 UpdateCommandRequest : public ProtoMessage {
public:
uint32_t key{0};
enums::UpdateCommand command{};
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;
};
} // namespace api
} // namespace esphome

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