Merge pull request #8863 from esphome/bump-2025.5.0

2025.5.0
This commit is contained in:
Jesse Hills 2025-05-21 20:32:40 +12:00 committed by GitHub
commit cdcd1cd292
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
595 changed files with 33968 additions and 6738 deletions

View File

@ -114,4 +114,5 @@ config/
examples/ examples/
Dockerfile Dockerfile
.git/ .git/
tests/build/ tests/
.*

View File

@ -1,15 +1,11 @@
name: Build Image name: Build Image
inputs: inputs:
platform:
description: "Platform to build for"
required: true
example: "linux/amd64"
target: target:
description: "Target to build" description: "Target to build"
required: true required: true
example: "docker" example: "docker"
baseimg: build_type:
description: "Base image type" description: "Build type"
required: true required: true
example: "docker" example: "docker"
suffix: suffix:
@ -19,6 +15,11 @@ inputs:
description: "Version to build" description: "Version to build"
required: true required: true
example: "2023.12.0" example: "2023.12.0"
base_os:
description: "Base OS to use"
required: false
default: "debian"
example: "debian"
runs: runs:
using: "composite" using: "composite"
steps: steps:
@ -46,52 +47,52 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }} target: ${{ inputs.target }}
cache-from: type=gha cache-from: type=gha
cache-to: ${{ steps.cache-to.outputs.value }} cache-to: ${{ steps.cache-to.outputs.value }}
build-args: | build-args: |
BASEIMGTYPE=${{ inputs.baseimg }} BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }} BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: | outputs: |
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export ghcr digests - name: Export ghcr digests
shell: bash shell: bash
run: | run: |
mkdir -p /tmp/digests/${{ inputs.target }}/ghcr mkdir -p /tmp/digests/${{ inputs.build_type }}/ghcr
digest="${{ steps.build-ghcr.outputs.digest }}" digest="${{ steps.build-ghcr.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" touch "/tmp/digests/${{ inputs.build_type }}/ghcr/${digest#sha256:}"
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
platforms: ${{ inputs.platform }}
target: ${{ inputs.target }} target: ${{ inputs.target }}
cache-from: type=gha cache-from: type=gha
cache-to: ${{ steps.cache-to.outputs.value }} cache-to: ${{ steps.cache-to.outputs.value }}
build-args: | build-args: |
BASEIMGTYPE=${{ inputs.baseimg }} BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }} BUILD_VERSION=${{ inputs.version }}
BUILD_OS=${{ inputs.base_os }}
outputs: | outputs: |
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
- name: Export dockerhub digests - name: Export dockerhub digests
shell: bash shell: bash
run: | run: |
mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub mkdir -p /tmp/digests/${{ inputs.build_type }}/dockerhub
digest="${{ steps.build-dockerhub.outputs.digest }}" digest="${{ steps.build-dockerhub.outputs.digest }}"
touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" touch "/tmp/digests/${{ inputs.build_type }}/dockerhub/${digest#sha256:}"

View File

@ -17,7 +17,7 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@ -34,7 +34,7 @@ runs:
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .
- name: Create Python virtual environment - name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
@ -43,5 +43,5 @@ runs:
python -m venv venv python -m venv venv
./venv/Scripts/activate ./venv/Scripts/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .

View File

@ -17,7 +17,6 @@ updates:
docker-actions: docker-actions:
applies-to: version-updates applies-to: version-updates
patterns: patterns:
- "docker/setup-qemu-action"
- "docker/login-action" - "docker/login-action"
- "docker/setup-buildx-action" - "docker/setup-buildx-action"
- package-ecosystem: github-actions - package-ecosystem: github-actions

View File

@ -23,7 +23,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.11" python-version: "3.11"
@ -57,6 +57,17 @@ jobs:
event: 'REQUEST_CHANGES', 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.' 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: failure()
name: Show changes
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@v4.6.2
with:
name: generated-proto-files
path: |
esphome/components/api/api_pb2.*
esphome/components/api/api_pb2_service.*
- if: success() - if: success()
name: Dismiss review name: Dismiss review
uses: actions/github-script@v7.0.1 uses: actions/github-script@v7.0.1

View File

@ -37,12 +37,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-latest", "ubuntu-24.04-arm"] os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
build_type: ["ha-addon", "docker", "lint"] build_type:
- "ha-addon"
- "docker"
# - "lint"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@ -39,10 +39,10 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@ -58,7 +58,7 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . pip install -e .
ruff: ruff:
@ -165,6 +165,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
script/ci-custom.py script/ci-custom.py
script/build_codeowners.py --check script/build_codeowners.py --check
script/build_language_schema.py --check
pytest: pytest:
name: Run pytest name: Run pytest
@ -220,7 +221,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5.4.2
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -291,6 +292,11 @@ jobs:
name: Run script/clang-tidy for ESP32 IDF name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf pio_cache_key: tidyesp32-idf
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR
pio_cache_key: tidy-zephyr
ignore_errors: true
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@ -330,13 +336,13 @@ jobs:
- name: Run clang-tidy - name: Run clang-tidy
run: | run: |
. venv/bin/activate . venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }} script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
env: env:
# Also cache libdeps, store them in a ~/.platformio subfolder # Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes - name: Suggested changes
run: script/ci-suggest-changes run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()

View File

@ -18,6 +18,7 @@ jobs:
outputs: outputs:
tag: ${{ steps.tag.outputs.tag }} tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Get tag - name: Get tag
@ -27,6 +28,11 @@ jobs:
if [[ "${{ github.event_name }}" = "release" ]]; then if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}" TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false" BRANCH_BUILD="false"
if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
ENVIRONMENT="beta"
else
ENVIRONMENT="production"
fi
else else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')" today="$(date --utc '+%Y%m%d')"
@ -35,12 +41,15 @@ jobs:
if [[ "$BRANCH" != "dev" ]]; then if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}" TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true" BRANCH_BUILD="true"
ENVIRONMENT=""
else else
BRANCH_BUILD="false" BRANCH_BUILD="false"
ENVIRONMENT="dev"
fi fi
fi fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length # yamllint enable rule:line-length
deploy-pypi: deploy-pypi:
@ -53,46 +62,44 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: script/setup
- name: Build - name: Build
run: |- run: |-
pip3 install build pip3 install build
python3 -m build python3 -m build
- name: Publish - name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.4 uses: pypa/gh-action-pypi-publish@v1.12.4
with:
skip-existing: true
deploy-docker: deploy-docker:
name: Build ESPHome ${{ matrix.platform }} name: Build ESPHome ${{ matrix.platform.arch }}
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
permissions: permissions:
contents: read contents: read
packages: write packages: write
runs-on: ubuntu-latest runs-on: ${{ matrix.platform.os }}
needs: [init] needs: [init]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: platform:
- linux/amd64 - arch: amd64
- linux/arm64 os: "ubuntu-24.04"
- arch: arm64
os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.6.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.4.0
@ -109,45 +116,36 @@ jobs:
- name: Build docker - name: Build docker
uses: ./.github/actions/build-image uses: ./.github/actions/build-image
with: with:
platform: ${{ matrix.platform }} target: final
target: docker build_type: docker
baseimg: docker
suffix: "" suffix: ""
version: ${{ needs.init.outputs.tag }} version: ${{ needs.init.outputs.tag }}
- name: Build ha-addon - name: Build ha-addon
uses: ./.github/actions/build-image uses: ./.github/actions/build-image
with: with:
platform: ${{ matrix.platform }} target: final
target: hassio build_type: ha-addon
baseimg: hassio
suffix: "hassio" suffix: "hassio"
version: ${{ needs.init.outputs.tag }} version: ${{ needs.init.outputs.tag }}
- name: Build lint # - name: Build lint
uses: ./.github/actions/build-image # uses: ./.github/actions/build-image
with: # with:
platform: ${{ matrix.platform }} # target: lint
target: lint # build_type: lint
baseimg: docker # suffix: lint
suffix: lint # version: ${{ needs.init.outputs.tag }}
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 - name: Upload digests
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
with: with:
name: digests-${{ steps.sanitize.outputs.name }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
retention-days: 1 retention-days: 1
deploy-manifest: deploy-manifest:
name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} name: Publish ESPHome ${{ matrix.image.build_type }} to ${{ matrix.registry }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- init - init
@ -160,15 +158,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
image: image:
- title: "ha-addon" - build_type: "docker"
target: "hassio"
suffix: "hassio"
- title: "docker"
target: "docker"
suffix: "" suffix: ""
- title: "lint" - build_type: "ha-addon"
target: "lint" suffix: "hassio"
suffix: "lint" # - build_type: "lint"
# suffix: "lint"
registry: registry:
- ghcr - ghcr
- dockerhub - dockerhub
@ -176,7 +171,7 @@ jobs:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.1.7
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
@ -212,7 +207,7 @@ jobs:
done done
- name: Create manifest list and push - name: Create manifest list and push
working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }} working-directory: /tmp/digests/${{ matrix.image.build_type }}/${{ matrix.registry }}
run: | run: |
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
@ -243,3 +238,24 @@ jobs:
content: description content: description
} }
}) })
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Trigger Workflow
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "esphome-schema",
workflow_id: "generate-schemas.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

@ -22,7 +22,7 @@ jobs:
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.5.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: 3.12 python-version: 3.12

1
.gitignore vendored
View File

@ -143,3 +143,4 @@ sdkconfig.*
/components /components
/managed_components /managed_components
api-docs/

View File

@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.0 rev: v0.11.9
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -33,7 +33,7 @@ repos:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format

View File

@ -98,6 +98,7 @@ esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core
esphome/components/coolix/* @glmnet esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter esphome/components/copy/* @OttoWinter
esphome/components/cover/* @esphome/core esphome/components/cover/* @esphome/core
@ -168,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers esphome/components/gree/* @orestismers
@ -250,6 +251,7 @@ esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber esphome/components/m5stack_8angle/* @rnauber
esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell esphome/components/max17043/* @blacknell
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
@ -276,10 +278,11 @@ esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05 esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt esphome/components/mixer/speaker/* @kahrendt
esphome/components/mlx90393/* @functionpointer esphome/components/mlx90393/* @functionpointer
@ -317,6 +320,7 @@ esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931 esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
@ -324,7 +328,9 @@ esphome/components/pcf8563/* @KoenBreeman
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984 esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie esphome/components/pm1006/* @habbie
esphome/components/pm2005/* @andrewjswan
esphome/components/pmsa003i/* @sjtrny esphome/components/pmsa003i/* @sjtrny
esphome/components/pmsx003/* @ximex
esphome/components/pmwcs3/* @SeByDocKy esphome/components/pmwcs3/* @SeByDocKy
esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz
@ -393,6 +399,7 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
@ -424,6 +431,7 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984 esphome/components/tca9548a/* @andreashergert1984
@ -463,6 +471,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb esphome/components/uart/button/* @ssieb
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli esphome/components/ufire_ise/* @pvizeli

2877
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,131 +1,56 @@
# Build these with the build.py script ARG BUILD_VERSION=dev
# Example: ARG BUILD_OS=alpine
# python3 docker/build.py --tag dev --arch amd64 --build-type docker build ARG BUILD_BASE_VERSION=2025.04.0
ARG BUILD_TYPE=docker
# One of "docker", "hassio" FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
ARG BASEIMGTYPE=docker FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base
# https://github.com/hassio-addons/addon-debian-base/releases RUN git config --system --add safe.directory "*"
FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm
FROM debian:12.2-slim AS base-docker
FROM base-${BASEIMGTYPE} AS base ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14
ARG TARGETARCH COPY requirements.txt /
ARG TARGETVARIANT
# Note that --break-system-packages is used below because
# https://peps.python.org/pep-0668/ added a safety check that prevents
# installing packages with the same name as a system package. This is
# not a problem for us because we are not concerned about overwriting
# system packages because we are running in an isolated container.
RUN \ RUN \
apt-get update \ uv pip install --no-cache-dir \
# Use pinned versions so that we get updates with build caching -r /requirements.txt
&& apt-get install -y --no-install-recommends \
python3-pip=23.0.1+dfsg-1 \
python3-setuptools=66.1.1-1+deb12u1 \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u2 \
curl=7.88.1-10+deb12u12 \
openssh-client=1:9.2p1-2+deb12u5 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
patch=2.7.6-7 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
ENV \
# Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
# Store globally installed pio libs in /piolibs
PLATFORMIO_GLOBALLIB_DIR=/piolibs
RUN \ RUN \
pip3 install \ platformio settings set enable_telemetry No \
--break-system-packages --no-cache-dir \
# Keep platformio version in sync with requirements.txt
platformio==6.1.18 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \ && platformio settings set check_platformio_interval 1000000 \
&& mkdir -p /piolibs && mkdir -p /piolibs
# 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 /
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
# Fail on any non-zero status
set -e
# 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+deb12u4
libssl-dev=3.0.15-1~deb12u1
libffi-dev=3.4.4-1
cargo=0.66.0+ds1-1
pkg-config=1.8.1-1
"
LIB_DEPS="
libtiff6=4.5.0-6+deb12u1
libopenjp2-7=2.5.0-2+deb12u1
"
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then
apt-get update
apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_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" ]
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 / COPY script/platformio_install_deps.py platformio.ini /
RUN /platformio_install_deps.py /platformio.ini --libraries RUN /platformio_install_deps.py /platformio.ini --libraries
# Avoid unsafe git error when container user and file config volume permissions don't match ARG BUILD_VERSION
RUN git config --system --add safe.directory '*'
LABEL \
org.opencontainers.image.authors="The ESPHome Authors" \
org.opencontainers.image.title="ESPHome" \
org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
org.opencontainers.image.url="https://esphome.io/" \
org.opencontainers.image.documentation="https://esphome.io/" \
org.opencontainers.image.source="https://github.com/esphome/esphome" \
org.opencontainers.image.licenses="ESPHome" \
org.opencontainers.image.version=${BUILD_VERSION}
# ======================= docker-type image ======================= # ======================= docker-type image =======================
FROM base AS docker FROM base AS base-docker
# Copy esphome and install
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
# Settings for dashboard
ENV USERNAME="" PASSWORD=""
# Expose the dashboard to Docker # Expose the dashboard to Docker
EXPOSE 6052 EXPOSE 6052
# Run healthcheck (heartbeat) # Run healthcheck (heartbeat)
HEALTHCHECK --interval=30s --timeout=30s \ HEALTHCHECK --interval=30s --timeout=30s \
CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1 CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
COPY docker/docker_entrypoint.sh /entrypoint.sh COPY docker/docker_entrypoint.sh /entrypoint.sh
@ -139,43 +64,13 @@ ENTRYPOINT ["/entrypoint.sh"]
CMD ["dashboard", "/config"] CMD ["dashboard", "/config"]
ARG BUILD_VERSION=dev # ======================= ha-addon-type image =======================
FROM base AS base-ha-addon
# Labels
LABEL \
org.opencontainers.image.authors="The ESPHome Authors" \
org.opencontainers.image.title="ESPHome" \
org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
org.opencontainers.image.url="https://esphome.io/" \
org.opencontainers.image.documentation="https://esphome.io/" \
org.opencontainers.image.source="https://github.com/esphome/esphome" \
org.opencontainers.image.licenses="ESPHome" \
org.opencontainers.image.version=${BUILD_VERSION}
# ======================= hassio-type image =======================
FROM base AS hassio
RUN \
apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
nginx-light=1.22.1-9+deb12u1 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
ARG BUILD_VERSION=dev
# Copy root filesystem # Copy root filesystem
COPY docker/ha-addon-rootfs/ / COPY docker/ha-addon-rootfs/ /
# Copy esphome and install ARG BUILD_VERSION
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
# Labels
LABEL \ LABEL \
io.hass.name="ESPHome" \ io.hass.name="ESPHome" \
io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \ io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
@ -183,35 +78,9 @@ LABEL \
io.hass.version="${BUILD_VERSION}" io.hass.version="${BUILD_VERSION}"
# io.hass.arch is inherited from addon-debian-base # io.hass.arch is inherited from addon-debian-base
ARG BUILD_TYPE
FROM base-${BUILD_TYPE} AS final
# Copy esphome and install
COPY . /esphome
# ======================= lint-type image ======================= RUN uv pip install --no-cache-dir -e /esphome
FROM base AS lint
ENV \
PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
RUN \
curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \
&& echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \
&& apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
clang-format-13=1:13.0.1-11+b2 \
patch=2.7.6-7 \
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 \
clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
COPY requirements_test.txt /
RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt
VOLUME ["/esphome"]
WORKDIR /esphome

View File

@ -54,7 +54,7 @@ manifest_parser = subparsers.add_parser(
class DockerParams: class DockerParams:
build_to: str build_to: str
manifest_to: str manifest_to: str
baseimgtype: str build_type: str
platform: str platform: str
target: str target: str
@ -66,24 +66,19 @@ class DockerParams:
TYPE_LINT: "esphome/esphome-lint", TYPE_LINT: "esphome/esphome-lint",
}[build_type] }[build_type]
build_to = f"{prefix}-{arch}" build_to = f"{prefix}-{arch}"
baseimgtype = {
TYPE_DOCKER: "docker",
TYPE_HA_ADDON: "hassio",
TYPE_LINT: "docker",
}[build_type]
platform = { platform = {
ARCH_AMD64: "linux/amd64", ARCH_AMD64: "linux/amd64",
ARCH_AARCH64: "linux/arm64", ARCH_AARCH64: "linux/arm64",
}[arch] }[arch]
target = { target = {
TYPE_DOCKER: "docker", TYPE_DOCKER: "final",
TYPE_HA_ADDON: "hassio", TYPE_HA_ADDON: "final",
TYPE_LINT: "lint", TYPE_LINT: "lint",
}[build_type] }[build_type]
return cls( return cls(
build_to=build_to, build_to=build_to,
manifest_to=prefix, manifest_to=prefix,
baseimgtype=baseimgtype, build_type=build_type,
platform=platform, platform=platform,
target=target, target=target,
) )
@ -145,7 +140,7 @@ def main():
"buildx", "buildx",
"build", "build",
"--build-arg", "--build-arg",
f"BASEIMGTYPE={params.baseimgtype}", f"BUILD_TYPE={params.build_type}",
"--build-arg", "--build-arg",
f"BUILD_VERSION={args.tag}", f"BUILD_VERSION={args.tag}",
"--cache-from", "--cache-from",

View File

@ -43,7 +43,7 @@ from esphome.const import (
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, 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.log import AnsiFore, color, setup_log
from esphome.util import ( from esphome.util import (
get_serial_ports, get_serial_ports,
list_yaml_files, list_yaml_files,
@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
raise ValueError raise ValueError
break break
except ValueError: except ValueError:
safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
return options[opt - 1][1] return options[opt - 1][1]
@ -596,30 +596,30 @@ def command_update_all(args):
click.echo(f"{half_line}{middle_text}{half_line}") click.echo(f"{half_line}{middle_text}{half_line}")
for f in files: for f in files:
print(f"Updating {color(Fore.CYAN, f)}") print(f"Updating {color(AnsiFore.CYAN, f)}")
print("-" * twidth) print("-" * twidth)
print() print()
rc = run_external_process( rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
) )
if rc == 0: if rc == 0:
print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
success[f] = True success[f] = True
else: else:
print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
success[f] = False success[f] = False
print() print()
print() print()
print() print()
print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0 failed = 0
for f in files: for f in files:
if success[f]: if success[f]:
print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else: else:
print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1 failed += 1
return failed return failed
@ -645,7 +645,7 @@ def command_rename(args, config):
if c not in ALLOWED_NAME_CHARS: if c not in ALLOWED_NAME_CHARS:
print( print(
color( color(
Fore.BOLD_RED, AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: " f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
) )
@ -658,7 +658,9 @@ def command_rename(args, config):
yaml = yaml_util.load_yaml(CORE.config_path) yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print( print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
) )
return 1 return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME] old_name = yaml[CONF_ESPHOME][CONF_NAME]
@ -681,7 +683,7 @@ def command_rename(args, config):
) )
> 1 > 1
): ):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1 return 1
new_raw = re.sub( new_raw = re.sub(
@ -693,7 +695,7 @@ def command_rename(args, config):
new_path = os.path.join(CORE.config_dir, args.name + ".yaml") new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print( print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
) )
print() print()
@ -702,7 +704,7 @@ def command_rename(args, config):
rc = run_external_process("esphome", "config", new_path) rc = run_external_process("esphome", "config", new_path)
if rc != 0: if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path) os.remove(new_path)
return 1 return 1
@ -728,7 +730,7 @@ def command_rename(args, config):
if CORE.config_path != new_path: if CORE.config_path != new_path:
os.remove(CORE.config_path) os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS")) print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print() print()
return 0 return 0

View File

@ -114,13 +114,14 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
// fully off, disable output immediately // fully off, disable output immediately
this->gate_pin.digital_write(false); this->gate_pin.digital_write(false);
} else { } else {
auto min_us = this->cycle_time_us * this->min_power / 1000;
if (this->method == DIM_METHOD_TRAILING) { if (this->method == DIM_METHOD_TRAILING) {
this->enable_time_us = 1; // cannot be 0 this->enable_time_us = 1; // cannot be 0
this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535); // calculate time until disable in µs with integer arithmetic and take into account min_power
this->disable_time_us = std::max((uint32_t) 10, this->value * (this->cycle_time_us - min_us) / 65535 + min_us);
} else { } else {
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
// also take into account min_power // also take into account min_power
auto min_us = this->cycle_time_us * this->min_power / 1000;
this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
if (this->method == DIM_METHOD_LEADING_PULSE) { if (this->method == DIM_METHOD_LEADING_PULSE) {

View File

@ -47,9 +47,10 @@ SAMPLING_MODES = {
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t") adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
# pin to adc1 channel mapping # pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0, 36: adc1_channel_t.ADC1_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1, 37: adc1_channel_t.ADC1_CHANNEL_1,
@ -60,6 +61,41 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
34: adc1_channel_t.ADC1_CHANNEL_6, 34: adc1_channel_t.ADC1_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7, 35: adc1_channel_t.ADC1_CHANNEL_7,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc1_channel_t.ADC1_CHANNEL_1,
@ -72,6 +108,7 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc1_channel_t.ADC1_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc1_channel_t.ADC1_CHANNEL_1,
@ -84,40 +121,12 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc1_channel_t.ADC1_CHANNEL_9,
}, },
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
},
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
},
VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
},
} }
# pin to adc2 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
# TODO: add other variants # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0, 4: adc2_channel_t.ADC2_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1, 0: adc2_channel_t.ADC2_CHANNEL_1,
@ -130,6 +139,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
25: adc2_channel_t.ADC2_CHANNEL_8, 25: adc2_channel_t.ADC2_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9, 26: adc2_channel_t.ADC2_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc2_channel_t.ADC2_CHANNEL_1,
@ -142,6 +164,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc2_channel_t.ADC2_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc2_channel_t.ADC2_CHANNEL_1,
@ -154,12 +177,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc2_channel_t.ADC2_CHANNEL_9,
}, },
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
},
VARIANT_ESP32C2: {},
VARIANT_ESP32C6: {},
VARIANT_ESP32H2: {},
} }

View File

@ -34,7 +34,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_(
BASE_SCHEMA = ( BASE_SCHEMA = (
sensor.SENSOR_SCHEMA.extend( cv.Schema(
{ {
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,

View File

@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CODE, CONF_CODE,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_STATE, CONF_ON_STATE,
@ -12,6 +14,7 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
@ -78,12 +81,11 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition "AlarmControlPanelCondition", automation.Condition
) )
ALARM_CONTROL_PANEL_SCHEMA = ( _ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTAlarmControlPanelComponent mqtt.MQTTAlarmControlPanelComponent
), ),
@ -146,6 +148,33 @@ ALARM_CONTROL_PANEL_SCHEMA = (
) )
) )
def alarm_control_panel_schema(
class_: MockObjClass,
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.GenerateID(): cv.use_id(AlarmControlPanel), cv.GenerateID(): cv.use_id(AlarmControlPanel),
@ -209,6 +238,12 @@ async def register_alarm_control_panel(var, config):
await setup_alarm_control_panel_core_(var, config) await setup_alarm_control_panel_core_(var, config)
async def new_alarm_control_panel(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_alarm_control_panel(var, config)
return var
@automation.register_action( @automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
) )

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, cover from esphome.components import ble_client, cover
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PIN from esphome.const import CONF_PIN
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
@ -15,9 +15,9 @@ Am43Component = am43_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cover.COVER_SCHEMA.extend( cover.cover_schema(Am43Component)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Am43Component),
cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
} }
@ -28,9 +28,8 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
await cg.register_component(var, config) await cg.register_component(var, config)
await cover.register_cover(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)

View File

@ -14,7 +14,8 @@ void AnalogThresholdBinarySensor::setup() {
if (std::isnan(sensor_value)) { if (std::isnan(sensor_value)) {
this->publish_initial_state(false); this->publish_initial_state(false);
} else { } else {
this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f); this->publish_initial_state(sensor_value >=
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
} }
} }
@ -24,7 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) { this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state // if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) { if (!std::isnan(sensor_value)) {
this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_)); this->publish_state(sensor_value >=
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
} }
}); });
} }
@ -32,8 +34,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
void AnalogThresholdBinarySensor::dump_config() { void AnalogThresholdBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this); LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this);
LOG_SENSOR(" ", "Sensor", this->sensor_); LOG_SENSOR(" ", "Sensor", this->sensor_);
ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_); ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_.value());
ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_); ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_.value());
} }
} // namespace analog_threshold } // namespace analog_threshold

View File

@ -15,14 +15,13 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
void set_sensor(sensor::Sensor *analog_sensor); void set_sensor(sensor::Sensor *analog_sensor);
void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; } template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; } template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }
protected: protected:
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
float upper_threshold_; TemplatableValue<float> lower_threshold_{};
float lower_threshold_;
}; };
} // namespace analog_threshold } // namespace analog_threshold

View File

@ -18,11 +18,11 @@ CONFIG_SCHEMA = (
{ {
cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor),
cv.Required(CONF_THRESHOLD): cv.Any( cv.Required(CONF_THRESHOLD): cv.Any(
cv.float_, cv.templatable(cv.float_),
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_UPPER): cv.float_, cv.Required(CONF_UPPER): cv.templatable(cv.float_),
cv.Required(CONF_LOWER): cv.float_, cv.Required(CONF_LOWER): cv.templatable(cv.float_),
} }
), ),
), ),
@ -39,9 +39,11 @@ async def to_code(config):
sens = await cg.get_variable(config[CONF_SENSOR_ID]) sens = await cg.get_variable(config[CONF_SENSOR_ID])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if isinstance(config[CONF_THRESHOLD], float): if isinstance(config[CONF_THRESHOLD], dict):
cg.add(var.set_upper_threshold(config[CONF_THRESHOLD])) lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
cg.add(var.set_lower_threshold(config[CONF_THRESHOLD])) upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
else: else:
cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER])) lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER])) upper = lower
cg.add(var.set_upper_threshold(upper))
cg.add(var.set_lower_threshold(lower))

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT from esphome.const import CONF_UNIT_OF_MEASUREMENT
UNITS = { UNITS = {
"f": "f", "f": "f",
@ -17,9 +17,9 @@ Anova = anova_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(Anova)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Anova),
cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
} }
) )
@ -29,8 +29,7 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))

View File

@ -82,6 +82,19 @@ ACTIONS_SCHEMA = automation.validate_automation(
), ),
) )
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): validate_encryption_key,
}
)
def _encryption_schema(config):
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@ -95,11 +108,7 @@ CONFIG_SCHEMA = cv.All(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA, ): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, 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.Optional(CONF_ENCRYPTION): _encryption_schema,
{
cv.Required(CONF_KEY): validate_encryption_key,
}
),
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True single=True
), ),
@ -151,9 +160,17 @@ async def to_code(config):
config[CONF_ON_CLIENT_DISCONNECTED], config[CONF_ON_CLIENT_DISCONNECTED],
) )
if encryption_config := config.get(CONF_ENCRYPTION): if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
decoded = base64.b64decode(encryption_config[CONF_KEY]) if key := encryption_config.get(CONF_KEY):
cg.add(var.set_noise_psk(list(decoded))) decoded = base64.b64decode(key)
cg.add(var.set_noise_psk(list(decoded)))
else:
# No key provided, but encryption desired
# This will allow a plaintext client to provide a noise key,
# send it to the device, and then switch to noise.
# The key will be saved in flash and used for future connections
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.6") cg.add_library("esphome/noise-c", "0.1.6")
else: else:

View File

@ -31,24 +31,26 @@ service APIConnection {
option (needs_authentication) = false; option (needs_authentication) = false;
} }
rpc execute_service (ExecuteServiceRequest) returns (void) {} rpc execute_service (ExecuteServiceRequest) returns (void) {}
rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
rpc cover_command (CoverCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {}
rpc fan_command (FanCommandRequest) returns (void) {}
rpc light_command (LightCommandRequest) returns (void) {}
rpc switch_command (SwitchCommandRequest) returns (void) {}
rpc camera_image (CameraImageRequest) returns (void) {} rpc camera_image (CameraImageRequest) returns (void) {}
rpc climate_command (ClimateCommandRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {} rpc cover_command (CoverCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
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 date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) returns (void) {} rpc datetime_command (DateTimeCommandRequest) returns (void) {}
rpc fan_command (FanCommandRequest) returns (void) {}
rpc light_command (LightCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
rpc siren_command (SirenCommandRequest) returns (void) {}
rpc switch_command (SwitchCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {}
rpc valve_command (ValveCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@ -60,6 +62,7 @@ service APIConnection {
rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {} rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {}
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
@ -230,6 +233,9 @@ message DeviceInfoResponse {
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
string bluetooth_mac_address = 18; string bluetooth_mac_address = 18;
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19;
} }
message ListEntitiesRequest { message ListEntitiesRequest {
@ -650,10 +656,27 @@ message SubscribeLogsResponse {
option (no_delay) = false; option (no_delay) = false;
LogLevel level = 1; LogLevel level = 1;
string message = 3; bytes message = 3;
bool send_failed = 4; bool send_failed = 4;
} }
// ==================== NOISE ENCRYPTION ====================
message NoiseEncryptionSetKeyRequest {
option (id) = 124;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE";
bytes key = 1;
}
message NoiseEncryptionSetKeyResponse {
option (id) = 125;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_NOISE";
bool success = 1;
}
// ==================== HOMEASSISTANT.SERVICE ==================== // ==================== HOMEASSISTANT.SERVICE ====================
message SubscribeHomeassistantServicesRequest { message SubscribeHomeassistantServicesRequest {
option (id) = 34; option (id) = 34;
@ -889,6 +912,7 @@ message ClimateStateResponse {
float target_temperature = 4; float target_temperature = 4;
float target_temperature_low = 5; float target_temperature_low = 5;
float target_temperature_high = 6; float target_temperature_high = 6;
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
bool unused_legacy_away = 7; bool unused_legacy_away = 7;
ClimateAction action = 8; ClimateAction action = 8;
ClimateFanMode fan_mode = 9; ClimateFanMode fan_mode = 9;
@ -914,6 +938,7 @@ message ClimateCommandRequest {
float target_temperature_low = 7; float target_temperature_low = 7;
bool has_target_temperature_high = 8; bool has_target_temperature_high = 8;
float target_temperature_high = 9; float target_temperature_high = 9;
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
bool unused_has_legacy_away = 10; bool unused_has_legacy_away = 10;
bool unused_legacy_away = 11; bool unused_legacy_away = 11;
bool has_fan_mode = 12; bool has_fan_mode = 12;
@ -1016,6 +1041,49 @@ message SelectCommandRequest {
string state = 2; string state = 2;
} }
// ==================== SIREN ====================
message ListEntitiesSirenResponse {
option (id) = 55;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
repeated string tones = 7;
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
}
message SirenStateResponse {
option (id) = 56;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
fixed32 key = 1;
bool state = 2;
}
message SirenCommandRequest {
option (id) = 57;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_tone = 4;
string tone = 5;
bool has_duration = 6;
uint32 duration = 7;
bool has_volume = 8;
float volume = 9;
}
// ==================== LOCK ==================== // ==================== LOCK ====================
enum LockState { enum LockState {
@ -1185,8 +1253,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
message BluetoothServiceData { message BluetoothServiceData {
string uuid = 1; string uuid = 1;
repeated uint32 legacy_data = 2 [deprecated = true]; repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
bytes data = 3; // Changed in proto version 1.7 bytes data = 3; // Added in api version 1.7
} }
message BluetoothLEAdvertisementResponse { message BluetoothLEAdvertisementResponse {
option (id) = 67; option (id) = 67;
@ -1195,7 +1263,7 @@ message BluetoothLEAdvertisementResponse {
option (no_delay) = true; option (no_delay) = true;
uint64 address = 1; uint64 address = 1;
string name = 2; bytes name = 2;
sint32 rssi = 3; sint32 rssi = 3;
repeated string service_uuids = 4; repeated string service_uuids = 4;
@ -1451,7 +1519,38 @@ message BluetoothDeviceClearCacheResponse {
int32 error = 3; int32 error = 3;
} }
// ==================== PUSH TO TALK ==================== enum BluetoothScannerState {
BLUETOOTH_SCANNER_STATE_IDLE = 0;
BLUETOOTH_SCANNER_STATE_STARTING = 1;
BLUETOOTH_SCANNER_STATE_RUNNING = 2;
BLUETOOTH_SCANNER_STATE_FAILED = 3;
BLUETOOTH_SCANNER_STATE_STOPPING = 4;
BLUETOOTH_SCANNER_STATE_STOPPED = 5;
}
enum BluetoothScannerMode {
BLUETOOTH_SCANNER_MODE_PASSIVE = 0;
BLUETOOTH_SCANNER_MODE_ACTIVE = 1;
}
message BluetoothScannerStateResponse {
option(id) = 126;
option(source) = SOURCE_SERVER;
option(ifdef) = "USE_BLUETOOTH_PROXY";
BluetoothScannerState state = 1;
BluetoothScannerMode mode = 2;
}
message BluetoothScannerSetModeRequest {
option(id) = 127;
option(source) = SOURCE_CLIENT;
option(ifdef) = "USE_BLUETOOTH_PROXY";
BluetoothScannerMode mode = 1;
}
// ==================== VOICE ASSISTANT ====================
enum VoiceAssistantSubscribeFlag { enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,17 @@
#include "api_server.h" #include "api_server.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include <vector> #include <vector>
namespace esphome { namespace esphome {
namespace api { namespace api {
using send_message_t = bool(APIConnection *, void *); // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
using send_message_t = bool (APIConnection::*)(void *);
/* /*
This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
@ -30,10 +34,10 @@ class DeferredMessageQueue {
protected: protected:
void *source_; void *source_;
send_message_t *send_message_; send_message_t send_message_;
public: public:
DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {} DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
bool operator==(const DeferredMessage &test) const { bool operator==(const DeferredMessage &test) const {
return (source_ == test.source_ && send_message_ == test.send_message_); return (source_ == test.source_ && send_message_ == test.send_message_);
} }
@ -46,12 +50,13 @@ class DeferredMessageQueue {
APIConnection *api_connection_; APIConnection *api_connection_;
// helper for allowing only unique entries in the queue // helper for allowing only unique entries in the queue
void dmq_push_back_with_dedup_(void *source, send_message_t *send_message); void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
public: public:
DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {} DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
void process_queue(); void process_queue();
void defer(void *source, send_message_t *send_message); void defer(void *source, send_message_t send_message);
bool empty() const { return deferred_queue_.empty(); }
}; };
class APIConnection : public APIServerConnection { class APIConnection : public APIServerConnection {
@ -69,137 +74,213 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state); protected:
static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor); bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor);
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state);
bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor);
public:
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool send_cover_state(cover::Cover *cover); bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover); void send_cover_info(cover::Cover *cover);
static bool try_send_cover_state(APIConnection *api, void *v_cover);
static bool try_send_cover_info(APIConnection *api, void *v_cover);
void cover_command(const CoverCommandRequest &msg) override; void cover_command(const CoverCommandRequest &msg) override;
protected:
bool try_send_cover_state_(cover::Cover *cover);
bool try_send_cover_info_(cover::Cover *cover);
public:
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool send_fan_state(fan::Fan *fan); bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan); void send_fan_info(fan::Fan *fan);
static bool try_send_fan_state(APIConnection *api, void *v_fan);
static bool try_send_fan_info(APIConnection *api, void *v_fan);
void fan_command(const FanCommandRequest &msg) override; void fan_command(const FanCommandRequest &msg) override;
protected:
bool try_send_fan_state_(fan::Fan *fan);
bool try_send_fan_info_(fan::Fan *fan);
public:
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool send_light_state(light::LightState *light); bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light); void send_light_info(light::LightState *light);
static bool try_send_light_state(APIConnection *api, void *v_light);
static bool try_send_light_info(APIConnection *api, void *v_light);
void light_command(const LightCommandRequest &msg) override; void light_command(const LightCommandRequest &msg) override;
protected:
bool try_send_light_state_(light::LightState *light);
bool try_send_light_info_(light::LightState *light);
public:
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state); bool send_sensor_state(sensor::Sensor *sensor, float state);
void send_sensor_info(sensor::Sensor *sensor); void send_sensor_info(sensor::Sensor *sensor);
static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state); protected:
static bool try_send_sensor_info(APIConnection *api, void *v_sensor); bool try_send_sensor_state_(sensor::Sensor *sensor);
bool try_send_sensor_state_(sensor::Sensor *sensor, float state);
bool try_send_sensor_info_(sensor::Sensor *sensor);
public:
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state); bool send_switch_state(switch_::Switch *a_switch, bool state);
void send_switch_info(switch_::Switch *a_switch); void send_switch_info(switch_::Switch *a_switch);
static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
void switch_command(const SwitchCommandRequest &msg) override; void switch_command(const SwitchCommandRequest &msg) override;
protected:
bool try_send_switch_state_(switch_::Switch *a_switch);
bool try_send_switch_state_(switch_::Switch *a_switch, bool state);
bool try_send_switch_info_(switch_::Switch *a_switch);
public:
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor); void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state); protected:
static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor); bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor);
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state);
bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor);
public:
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera); void send_camera_info(esp32_camera::ESP32Camera *camera);
static bool try_send_camera_info(APIConnection *api, void *v_camera);
void camera_image(const CameraImageRequest &msg) override; void camera_image(const CameraImageRequest &msg) override;
protected:
bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
public:
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate); bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate); void send_climate_info(climate::Climate *climate);
static bool try_send_climate_state(APIConnection *api, void *v_climate);
static bool try_send_climate_info(APIConnection *api, void *v_climate);
void climate_command(const ClimateCommandRequest &msg) override; void climate_command(const ClimateCommandRequest &msg) override;
protected:
bool try_send_climate_state_(climate::Climate *climate);
bool try_send_climate_info_(climate::Climate *climate);
public:
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state); bool send_number_state(number::Number *number, float state);
void send_number_info(number::Number *number); void send_number_info(number::Number *number);
static bool try_send_number_state(APIConnection *api, void *v_number);
static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
static bool try_send_number_info(APIConnection *api, void *v_number);
void number_command(const NumberCommandRequest &msg) override; void number_command(const NumberCommandRequest &msg) override;
protected:
bool try_send_number_state_(number::Number *number);
bool try_send_number_state_(number::Number *number, float state);
bool try_send_number_info_(number::Number *number);
public:
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date); bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date); void send_date_info(datetime::DateEntity *date);
static bool try_send_date_state(APIConnection *api, void *v_date);
static bool try_send_date_info(APIConnection *api, void *v_date);
void date_command(const DateCommandRequest &msg) override; void date_command(const DateCommandRequest &msg) override;
protected:
bool try_send_date_state_(datetime::DateEntity *date);
bool try_send_date_info_(datetime::DateEntity *date);
public:
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time); bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time); void send_time_info(datetime::TimeEntity *time);
static bool try_send_time_state(APIConnection *api, void *v_time);
static bool try_send_time_info(APIConnection *api, void *v_time);
void time_command(const TimeCommandRequest &msg) override; void time_command(const TimeCommandRequest &msg) override;
protected:
bool try_send_time_state_(datetime::TimeEntity *time);
bool try_send_time_info_(datetime::TimeEntity *time);
public:
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime); bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime); void send_datetime_info(datetime::DateTimeEntity *datetime);
static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
void datetime_command(const DateTimeCommandRequest &msg) override; void datetime_command(const DateTimeCommandRequest &msg) override;
protected:
bool try_send_datetime_state_(datetime::DateTimeEntity *datetime);
bool try_send_datetime_info_(datetime::DateTimeEntity *datetime);
public:
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state); bool send_text_state(text::Text *text, std::string state);
void send_text_info(text::Text *text); void send_text_info(text::Text *text);
static bool try_send_text_state(APIConnection *api, void *v_text);
static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
static bool try_send_text_info(APIConnection *api, void *v_text);
void text_command(const TextCommandRequest &msg) override; void text_command(const TextCommandRequest &msg) override;
protected:
bool try_send_text_state_(text::Text *text);
bool try_send_text_state_(text::Text *text, std::string state);
bool try_send_text_info_(text::Text *text);
public:
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state); bool send_select_state(select::Select *select, std::string state);
void send_select_info(select::Select *select); void send_select_info(select::Select *select);
static bool try_send_select_state(APIConnection *api, void *v_select);
static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
static bool try_send_select_info(APIConnection *api, void *v_select);
void select_command(const SelectCommandRequest &msg) override; void select_command(const SelectCommandRequest &msg) override;
protected:
bool try_send_select_state_(select::Select *select);
bool try_send_select_state_(select::Select *select, std::string state);
bool try_send_select_info_(select::Select *select);
public:
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
void send_button_info(button::Button *button); void send_button_info(button::Button *button);
static bool try_send_button_info(APIConnection *api, void *v_button);
void button_command(const ButtonCommandRequest &msg) override; void button_command(const ButtonCommandRequest &msg) override;
protected:
bool try_send_button_info_(button::Button *button);
public:
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state); bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
void send_lock_info(lock::Lock *a_lock); void send_lock_info(lock::Lock *a_lock);
static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
void lock_command(const LockCommandRequest &msg) override; void lock_command(const LockCommandRequest &msg) override;
protected:
bool try_send_lock_state_(lock::Lock *a_lock);
bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state);
bool try_send_lock_info_(lock::Lock *a_lock);
public:
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve); bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve); void send_valve_info(valve::Valve *valve);
static bool try_send_valve_state(APIConnection *api, void *v_valve);
static bool try_send_valve_info(APIConnection *api, void *v_valve);
void valve_command(const ValveCommandRequest &msg) override; void valve_command(const ValveCommandRequest &msg) override;
protected:
bool try_send_valve_state_(valve::Valve *valve);
bool try_send_valve_info_(valve::Valve *valve);
public:
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player); void send_media_player_info(media_player::MediaPlayer *media_player);
static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override; void media_player_command(const MediaPlayerCommandRequest &msg) override;
protected:
bool try_send_media_player_state_(media_player::MediaPlayer *media_player);
bool try_send_media_player_info_(media_player::MediaPlayer *media_player);
public:
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line); bool try_send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
@ -221,6 +302,7 @@ class APIConnection : public APIServerConnection {
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
const SubscribeBluetoothConnectionsFreeRequest &msg) override; const SubscribeBluetoothConnectionsFreeRequest &msg) override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
@ -245,25 +327,37 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
protected:
bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
public:
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, std::string event_type); void send_event(event::Event *event, std::string event_type);
void send_event_info(event::Event *event); void send_event_info(event::Event *event);
static bool try_send_event(APIConnection *api, void *v_event);
static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type); protected:
static bool try_send_event_info(APIConnection *api, void *v_event); bool try_send_event_(event::Event *event);
bool try_send_event_(event::Event *event, std::string event_type);
bool try_send_event_info_(event::Event *event);
public:
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update); bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update); void send_update_info(update::UpdateEntity *update);
static bool try_send_update_state(APIConnection *api, void *v_update);
static bool try_send_update_info(APIConnection *api, void *v_update);
void update_command(const UpdateCommandRequest &msg) override; void update_command(const UpdateCommandRequest &msg) override;
protected:
bool try_send_update_state_(update::UpdateEntity *update);
bool try_send_update_info_(update::UpdateEntity *update);
public:
#endif #endif
void on_disconnect_response(const DisconnectResponse &value) override; void on_disconnect_response(const DisconnectResponse &value) override;
@ -300,6 +394,9 @@ class APIConnection : public APIServerConnection {
return {}; return {};
} }
void execute_service(const ExecuteServiceRequest &msg) override; void execute_service(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
#endif
bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
bool is_connection_setup() override { bool is_connection_setup() override {
@ -308,11 +405,20 @@ class APIConnection : public APIServerConnection {
void on_fatal_error() override; void on_fatal_error() override;
void on_unauthenticated_access() override; void on_unauthenticated_access() override;
void on_no_setup_connection() override; void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer() override { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen // FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear(); this->proto_write_buffer_.clear();
// Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
return {&this->proto_write_buffer_}; return {&this->proto_write_buffer_};
} }
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; } std::string get_client_combined_info() const { return this->client_combined_info_; }
@ -320,6 +426,99 @@ class APIConnection : public APIServerConnection {
protected: protected:
friend APIServer; friend APIServer;
/**
* Generic send entity state method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This is the base version for entities that use their current state.
*
* @param entity The entity to send state for
* @param try_send_func The function that tries to send the state
* @return True on success or message deferred, false if subscription check failed
*/
bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return true;
}
this->deferred_message_queue_.defer(entity, try_send_func);
return true;
}
/**
* Send entity state method that handles explicit state values.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This method accepts a state parameter to be used instead of the entity's current state.
* It attempts to send the state with the provided value first, and if that fails due to buffer constraints,
* it defers the entity for later processing using the entity-only function.
*
* @tparam EntityT The entity type
* @tparam StateT Type of the state parameter
* @tparam Args Additional argument types (if any)
* @param entity The entity to send state for
* @param try_send_entity_func The function that tries to send the state with entity pointer only
* @param try_send_state_func The function that tries to send the state with entity and state parameters
* @param state The state value to send
* @param args Additional arguments to pass to the try_send_state_func
* @return True on success or message deferred, false if subscription check failed
*/
template<typename EntityT, typename StateT, typename... Args>
bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *),
bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state,
Args... args) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) {
return true;
}
this->deferred_message_queue_.defer(entity, reinterpret_cast<send_message_t>(try_send_entity_func));
return true;
}
/**
* Generic send entity info method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* @param entity The entity to send info for
* @param try_send_func The function that tries to send the info
*/
void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return;
}
this->deferred_message_queue_.defer(entity, try_send_func);
}
/**
* Generic function for generating entity info response messages.
* This is used to reduce duplication in the try_send_*_info functions.
*
* @param entity The entity to generate info for
* @param response The response object
* @param send_response_func Function pointer to send the response
* @return True if the message was sent successfully
*/
template<typename ResponseT>
bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response,
bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
// Send the response using the provided send method
return (this->*send_response_func)(response);
}
bool send_(const void *buf, size_t len, bool force); bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState { enum class ConnectionState {

View File

@ -5,6 +5,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "proto.h" #include "proto.h"
#include "api_pb2_size.h"
#include <cstring> #include <cstring>
namespace esphome { namespace esphome {
@ -72,6 +73,91 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN"; return "UNKNOWN";
} }
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
StateEnum failed_state) {
// This method writes data to socket or buffers it
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf.empty()) {
// try to empty tx_buf first
while (!tx_buf.empty()) {
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
}
}
if (!tx_buf.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
}
ssize_t sent = socket->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
} else if (sent == -1) {
// an error occurred
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t remaining = total_write_len - sent;
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + remaining);
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK; // Success, data buffered
}
return APIError::OK; // Success, all data sent
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
@ -407,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
std::vector<uint8_t> data; std::vector<uint8_t> data;
data.resize(reason.length() + 1); data.resize(reason.length() + 1);
data[0] = 0x01; // failure data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i]; // Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
} }
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
@ -471,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK; return APIError::OK;
} }
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
int err; int err;
APIError aerr; APIError aerr;
aerr = state_action_(); aerr = state_action_();
@ -483,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding
size_t payload_len = raw_buffer->size() - frame_header_padding_;
size_t padding = 0; size_t padding = 0;
size_t msg_len = 4 + payload_len + padding; size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator // We need to resize to include MAC space, but we already reserved it in create_buffer
// tmpbuf[1], tmpbuf[2] to be set later raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Write the noise header in the padded area
// Buffer layout:
// [0] - 0x01 indicator byte
// [1-2] - Size of encrypted payload (filled after encryption)
// [3-4] - Message type (encrypted)
// [5-6] - Payload length (encrypted)
// [7...] - Actual payload data (encrypted)
uint8_t *buf_start = raw_buffer->data();
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set later after encryption
const uint8_t msg_offset = 3; const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4; buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
tmpbuf[msg_offset + 1] = (uint8_t) type; buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
tmpbuf[msg_offset + 3] = (uint8_t) payload_len; // payload data is already in the buffer starting at position 7
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf); err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;
@ -516,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
} }
size_t total_len = 3 + mbuf.size; size_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t) (mbuf.size >> 8); buf_start[1] = (uint8_t) (mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size; buf_start[2] = (uint8_t) mbuf.size;
struct iovec iov; struct iovec iov;
iov.iov_base = &tmpbuf[0]; // Point iov_base to the beginning of the buffer (no unused padding in Noise)
// We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
iov.iov_base = buf_start;
iov.iov_len = total_len; iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled // write raw to not have two packets sent if NAGLE disabled
@ -546,71 +642,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() {
return APIError::OK; return APIError::OK;
} }
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
if (iovcnt == 0)
return APIError::OK;
APIError aerr;
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
for (int i = 0; i < iovcnt; i++) {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK;
}
ssize_t sent = socket_->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
for (int i = 0; i < iovcnt; i++) {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK;
} else if (sent == -1) {
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
uint8_t header[3]; uint8_t header[3];
header[0] = 0x01; // indicator header[0] = 0x01; // indicator
@ -697,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_SPLIT_FAILED; return APIError::HANDSHAKESTATE_SPLIT_FAILED;
} }
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!"); HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_); noise_handshakestate_free(handshake_);
handshake_ = nullptr; handshake_ = nullptr;
@ -744,6 +777,11 @@ void noise_rand_bytes(void *output, size_t len) {
} }
} }
} }
// Explicit template instantiation for Noise
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
@ -804,6 +842,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
uint8_t data; uint8_t data;
// Reading one byte at a time is fastest in practice for ESP32 when
// there is no data on the wire (which is the common case).
// This results in faster failure detection compared to
// attempting to read multiple bytes at once.
ssize_t received = socket_->read(&data, 1); ssize_t received = socket_->read(&data, 1);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
@ -817,27 +859,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_header_buf_.push_back(data);
// try parse header // Successfully read a byte
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED; // Process byte according to current buffer position
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
return APIError::BAD_INDICATOR; if (data != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", data);
return APIError::BAD_INDICATOR;
}
// We don't store the indicator byte, just increment position
rx_header_buf_pos_ = 1; // Set to 1 directly
continue; // Need more bytes before we can parse
} }
size_t i = 1; // Check buffer overflow before storing
if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Store byte in buffer (adjust index to account for skipped indicator byte)
rx_header_buf_[rx_header_buf_pos_ - 1] = data;
// Increment position after storing
rx_header_buf_pos_++;
// Case 3: If we only have one varint byte, we need more
if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
continue; // Need more bytes before we can parse
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) but not stored
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// First 1-3 bytes: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's 65535
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// Remaining 1-2 bytes: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
uint32_t consumed = 0; uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
if (!msg_size_varint.has_value()) { if (!msg_size_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
i += consumed;
rx_header_parsed_len_ = msg_size_varint->as_uint32(); rx_header_parsed_len_ = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) { if (!msg_type_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
@ -883,7 +958,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// consume msg // consume msg
rx_buf_ = {}; rx_buf_ = {};
rx_buf_len_ = 0; rx_buf_len_ = 0;
rx_header_buf_.clear(); rx_header_buf_pos_ = 0;
rx_header_parsed_ = false; rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
@ -927,26 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK; return APIError::OK;
} }
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
std::vector<uint8_t> header; std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
header.push_back(0x00); // Message data starts after padding (frame_header_padding_ = 6)
ProtoVarInt(payload_len).encode(header); size_t payload_len = raw_buffer->size() - frame_header_padding_;
ProtoVarInt(type).encode(header);
struct iovec iov[2]; // Calculate varint sizes for header components
iov[0].iov_base = &header[0]; size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
iov[0].iov_len = header.size(); size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
if (payload_len == 0) { size_t total_header_len = 1 + size_varint_len + type_varint_len;
return write_raw_(iov, 1);
if (total_header_len > frame_header_padding_) {
// Header is too large to fit in the padding
return APIError::BAD_ARG;
} }
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2); // Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
size_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
struct iovec iov;
// Point iov_base to the beginning of our header (skip unused padding)
// This ensures we only send the actual header and payload, not the empty padding bytes
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
return write_raw_(&iov, 1);
} }
APIError APIPlaintextFrameHelper::try_send_tx_buf_() { APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf // try send from tx_buf
@ -966,71 +1081,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
return APIError::OK; return APIError::OK;
} }
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
if (iovcnt == 0)
return APIError::OK;
APIError aerr;
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
for (int i = 0; i < iovcnt; i++) {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK;
}
ssize_t sent = socket_->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
for (int i = 0; i < iovcnt; i++) {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK;
} else if (sent == -1) {
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APIPlaintextFrameHelper::close() { APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED; state_ = State::CLOSED;
@ -1048,6 +1098,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
} }
return APIError::OK; return APIError::OK;
} }
// Explicit template instantiation for Plaintext
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
#endif // USE_API_PLAINTEXT #endif // USE_API_PLAINTEXT
} // namespace api } // namespace api

View File

@ -16,6 +16,8 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
class ProtoWriteBuffer;
struct ReadPacketBuffer { struct ReadPacketBuffer {
std::vector<uint8_t> container; std::vector<uint8_t> container;
uint16_t type; uint16_t type;
@ -65,26 +67,46 @@ class APIFrameHelper {
virtual APIError loop() = 0; virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0; virtual bool can_write_without_blocking() = 0;
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
virtual std::string getpeername() = 0; virtual std::string getpeername() = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual APIError close() = 0; virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0; virtual APIError shutdown(int how) = 0;
// Give this helper a name for logging // Give this helper a name for logging
virtual void set_log_info(std::string info) = 0; virtual void set_log_info(std::string info) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
protected:
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper { class APINoiseFrameHelper : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} : socket_(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override; ~APINoiseFrameHelper() override;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); } std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen); return this->socket_->getpeername(addr, addrlen);
@ -93,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override; APIError shutdown(int how) override;
// Give this helper a name for logging // Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); } void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
struct ParsedFrame { struct ParsedFrame {
@ -103,7 +129,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len); APIError write_frame_(const uint8_t *data, size_t len);
APIError write_raw_(const struct iovec *iov, int iovcnt); inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason); void send_explicit_handshake_reject_(const std::string &reason);
@ -111,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::string info_; std::string info_;
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0; size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
@ -141,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper { class APIPlaintextFrameHelper : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {} APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default; ~APIPlaintextFrameHelper() override = default;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); } std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen); return this->socket_->getpeername(addr, addrlen);
@ -156,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override; APIError shutdown(int how) override;
// Give this helper a name for logging // Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); } void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
struct ParsedFrame { struct ParsedFrame {
@ -164,12 +206,23 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError try_send_tx_buf_();
APIError write_raw_(const struct iovec *iov, int iovcnt); inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::string info_; std::string info_;
std::vector<uint8_t> rx_header_buf_; // Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need:
// 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false; bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0; uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0; uint32_t rx_header_parsed_len_ = 0;

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <array> #include <array>
#include <cstdint>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
namespace esphome { namespace esphome {
@ -11,11 +11,20 @@ using psk_t = std::array<uint8_t, 32>;
class APINoiseContext { class APINoiseContext {
public: public:
void set_psk(psk_t psk) { psk_ = psk; } void set_psk(psk_t psk) {
const psk_t &get_psk() const { return psk_; } this->psk_ = psk;
bool has_psk = false;
for (auto i : psk) {
has_psk |= i;
}
this->has_psk_ = has_psk;
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
protected: protected:
psk_t psk_; psk_t psk_{};
bool has_psk_{false};
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
// This file was automatically generated with a tool. // This file was automatically generated with a tool.
// See scripts/api_protobuf/api_protobuf.py // See script/api_protobuf/api_protobuf.py
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -179,6 +179,16 @@ bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorSt
bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) { bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) {
return this->send_message_<SubscribeLogsResponse>(msg, 29); return this->send_message_<SubscribeLogsResponse>(msg, 29);
} }
#ifdef USE_API_NOISE
#endif
#ifdef USE_API_NOISE
bool APIServerConnectionBase::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_noise_encryption_set_key_response: %s", msg.dump().c_str());
#endif
return this->send_message_<NoiseEncryptionSetKeyResponse>(msg, 125);
}
#endif
bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
@ -282,6 +292,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
#endif #endif
#ifdef USE_SIREN
bool APIServerConnectionBase::send_list_entities_siren_response(const ListEntitiesSirenResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_siren_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesSirenResponse>(msg, 55);
}
#endif
#ifdef USE_SIREN
bool APIServerConnectionBase::send_siren_state_response(const SirenStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_siren_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<SirenStateResponse>(msg, 56);
}
#endif
#ifdef USE_SIREN
#endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) { bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -462,6 +490,16 @@ bool APIServerConnectionBase::send_bluetooth_device_clear_cache_response(const B
return this->send_message_<BluetoothDeviceClearCacheResponse>(msg, 88); return this->send_message_<BluetoothDeviceClearCacheResponse>(msg, 88);
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
bool APIServerConnectionBase::send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_bluetooth_scanner_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<BluetoothScannerStateResponse>(msg, 126);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@ -883,6 +921,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_select_command_request(msg); this->on_select_command_request(msg);
#endif
break;
}
case 57: {
#ifdef USE_SIREN
SirenCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
#endif
this->on_siren_command_request(msg);
#endif #endif
break; break;
} }
@ -1191,6 +1240,28 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
#endif #endif
this->on_voice_assistant_set_configuration(msg); this->on_voice_assistant_set_configuration(msg);
#endif
break;
}
case 124: {
#ifdef USE_API_NOISE
NoiseEncryptionSetKeyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
#endif
this->on_noise_encryption_set_key_request(msg);
#endif
break;
}
case 127: {
#ifdef USE_BLUETOOTH_PROXY
BluetoothScannerSetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str());
#endif
this->on_bluetooth_scanner_set_mode_request(msg);
#endif #endif
break; break;
} }
@ -1311,8 +1382,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
} }
this->execute_service(msg); this->execute_service(msg);
} }
#ifdef USE_COVER #ifdef USE_API_NOISE
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
this->on_no_setup_connection(); this->on_no_setup_connection();
return; return;
@ -1321,11 +1392,14 @@ void APIServerConnection::on_cover_command_request(const CoverCommandRequest &ms
this->on_unauthenticated_access(); this->on_unauthenticated_access();
return; return;
} }
this->cover_command(msg); NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
if (!this->send_noise_encryption_set_key_response(ret)) {
this->on_fatal_error();
}
} }
#endif #endif
#ifdef USE_FAN #ifdef USE_BUTTON
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
this->on_no_setup_connection(); this->on_no_setup_connection();
return; return;
@ -1334,33 +1408,7 @@ void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
this->on_unauthenticated_access(); this->on_unauthenticated_access();
return; return;
} }
this->fan_command(msg); this->button_command(msg);
}
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->light_command(msg);
}
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->switch_command(msg);
} }
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
@ -1389,8 +1437,8 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
this->climate_command(msg); this->climate_command(msg);
} }
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
this->on_no_setup_connection(); this->on_no_setup_connection();
return; return;
@ -1399,85 +1447,7 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
this->on_unauthenticated_access(); this->on_unauthenticated_access();
return; return;
} }
this->number_command(msg); this->cover_command(msg);
}
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->text_command(msg);
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->select_command(msg);
}
#endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->button_command(msg);
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->lock_command(msg);
}
#endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->valve_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@ -1493,19 +1463,6 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg)
this->date_command(msg); this->date_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->time_command(msg);
}
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
@ -1519,6 +1476,136 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
this->datetime_command(msg); this->datetime_command(msg);
} }
#endif #endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->fan_command(msg);
}
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->light_command(msg);
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->lock_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->number_command(msg);
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->select_command(msg);
}
#endif
#ifdef USE_SIREN
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->siren_command(msg);
}
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->switch_command(msg);
}
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->text_command(msg);
}
#endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->time_command(msg);
}
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
@ -1532,6 +1619,19 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
this->update_command(msg); this->update_command(msg);
} }
#endif #endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->valve_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) { const SubscribeBluetoothLEAdvertisementsRequest &msg) {
@ -1668,6 +1768,19 @@ void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
this->unsubscribe_bluetooth_le_advertisements(msg); this->unsubscribe_bluetooth_le_advertisements(msg);
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->bluetooth_scanner_set_mode(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {

View File

@ -1,5 +1,5 @@
// This file was automatically generated with a tool. // This file was automatically generated with a tool.
// See scripts/api_protobuf/api_protobuf.py // See script/api_protobuf/api_protobuf.py
#pragma once #pragma once
#include "api_pb2.h" #include "api_pb2.h"
@ -83,6 +83,12 @@ class APIServerConnectionBase : public ProtoService {
#endif #endif
virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
bool send_subscribe_logs_response(const SubscribeLogsResponse &msg); bool send_subscribe_logs_response(const SubscribeLogsResponse &msg);
#ifdef USE_API_NOISE
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
#endif
#ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg);
#endif
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg); bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg);
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
@ -130,6 +136,15 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_SELECT #ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){}; virtual void on_select_command_request(const SelectCommandRequest &value){};
#endif #endif
#ifdef USE_SIREN
bool send_list_entities_siren_response(const ListEntitiesSirenResponse &msg);
#endif
#ifdef USE_SIREN
bool send_siren_state_response(const SirenStateResponse &msg);
#endif
#ifdef USE_SIREN
virtual void on_siren_command_request(const SirenCommandRequest &value){};
#endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg); bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg);
#endif #endif
@ -228,6 +243,12 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg); bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg);
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
bool send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg);
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
#endif #endif
@ -349,17 +370,11 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#ifdef USE_COVER #ifdef USE_API_NOISE
virtual void cover_command(const CoverCommandRequest &msg) = 0; virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif #endif
#ifdef USE_FAN #ifdef USE_BUTTON
virtual void fan_command(const FanCommandRequest &msg) = 0; virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_LIGHT
virtual void light_command(const LightCommandRequest &msg) = 0;
#endif
#ifdef USE_SWITCH
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
virtual void camera_image(const CameraImageRequest &msg) = 0; virtual void camera_image(const CameraImageRequest &msg) = 0;
@ -367,39 +382,51 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
virtual void climate_command(const ClimateCommandRequest &msg) = 0; virtual void climate_command(const ClimateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
virtual void number_command(const NumberCommandRequest &msg) = 0; virtual void cover_command(const CoverCommandRequest &msg) = 0;
#endif
#ifdef USE_TEXT
virtual void text_command(const TextCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual void date_command(const DateCommandRequest &msg) = 0; virtual void date_command(const DateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_FAN
virtual void fan_command(const FanCommandRequest &msg) = 0;
#endif
#ifdef USE_LIGHT
virtual void light_command(const LightCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif
#ifdef USE_NUMBER
virtual void number_command(const NumberCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_SIREN
virtual void siren_command(const SirenCommandRequest &msg) = 0;
#endif
#ifdef USE_SWITCH
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
#endif
#ifdef USE_TEXT
virtual void text_command(const TextCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
virtual void update_command(const UpdateCommandRequest &msg) = 0; virtual void update_command(const UpdateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif #endif
@ -431,6 +458,9 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif #endif
@ -457,17 +487,11 @@ class APIServerConnection : public APIServerConnectionBase {
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
void on_get_time_request(const GetTimeRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override;
void on_execute_service_request(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#ifdef USE_COVER #ifdef USE_API_NOISE
void on_cover_command_request(const CoverCommandRequest &msg) override; void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
#ifdef USE_FAN #ifdef USE_BUTTON
void on_fan_command_request(const FanCommandRequest &msg) override; void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &msg) override;
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
void on_camera_image_request(const CameraImageRequest &msg) override; void on_camera_image_request(const CameraImageRequest &msg) override;
@ -475,39 +499,51 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_command_request(const ClimateCommandRequest &msg) override; void on_climate_command_request(const ClimateCommandRequest &msg) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
void on_number_command_request(const NumberCommandRequest &msg) override; void on_cover_command_request(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_command_request(const DateCommandRequest &msg) override; void on_date_command_request(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &msg) override; void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_FAN
void on_fan_command_request(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_SIREN
void on_siren_command_request(const SirenCommandRequest &msg) override;
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update_command_request(const UpdateCommandRequest &msg) override; void on_update_command_request(const UpdateCommandRequest &msg) override;
#endif #endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif #endif
@ -539,6 +575,9 @@ class APIServerConnection : public APIServerConnectionBase {
void on_unsubscribe_bluetooth_le_advertisements_request( void on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif #endif

View File

@ -0,0 +1,361 @@
#pragma once
#include "proto.h"
#include <cstdint>
#include <string>
namespace esphome {
namespace api {
class ProtoSize {
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides static methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be
* efficient for the common case where many fields have default values.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Early-return optimization for zero/default values
* - Direct total_size updates to avoid unnecessary additions
* - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding
if (value < 128)
return 1; // 7 bits, common case for small values
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
* @param value The uint64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint64_t value) {
// Handle common case of values fitting in uint32_t (vast majority of use cases)
if (value <= UINT32_MAX) {
return varint(static_cast<uint32_t>(value));
}
// For larger values, determine size based on highest bit position
if (value < (1ULL << 35)) {
return 5; // 35 bits
} else if (value < (1ULL << 42)) {
return 6; // 42 bits
} else if (value < (1ULL << 49)) {
return 7; // 49 bits
} else if (value < (1ULL << 56)) {
return 8; // 56 bits
} else if (value < (1ULL << 63)) {
return 9; // 63 bits
} else {
return 10; // 64 bits (maximum for uint64_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
*
* Special handling is needed for negative values, which are sign-extended to 64 bits
* in Protocol Buffers, resulting in a 10-byte varint.
*
* @param value The int32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int32_t value) {
// Negative values are sign-extended to 64 bits in protocol buffers,
// which always results in a 10-byte varint for negative int32
if (value < 0) {
return 10; // Negative int32 is always 10 bytes long
}
// For non-negative values, use the uint32_t implementation
return varint(static_cast<uint32_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
*
* @param value The int64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int64_t value) {
// For int64_t, we convert to uint64_t and calculate the size
// This works because the bit pattern determines the encoding size,
// and we've handled negative int32 values as a special case above
return varint(static_cast<uint64_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode a field ID and wire type
*
* @param field_id The field identifier
* @param type The wire type value (from the WireType enum in the protobuf spec)
* @return The number of bytes needed to encode the field ID and wire type
*/
static inline uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
return varint(tag);
}
/**
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
*
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
// Skip calculation if value is false and not forced
if (!value && !force) {
return; // No need to update total_size
}
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a fixed field to the total message size
*
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
*
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
* @param is_nonzero Whether the value is non-zero
*/
template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
bool force = false) {
// Skip calculation if value is zero and not forced
if (!is_nonzero && !force) {
return; // No need to update total_size
}
// Fixed fields always take exactly NumBytes
total_size += field_id_size + NumBytes;
}
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint64 field to the total message size
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size
*/
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
bool force = false) {
// Skip calculation if string is empty and not forced
if (str.empty() && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero or force is true.
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
bool force = false) {
// Skip calculation if nested message is empty and not forced
if (nested_size == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This templated version directly takes a message object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @tparam MessageType The type of the nested message (inferred from parameter)
* @param message The nested message object
*/
template<typename MessageType>
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
bool force = false) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size, force);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// For repeated fields, always use force=true
for (const auto &message : messages) {
add_message_object(total_size, field_id_size, message, true);
}
}
};
} // namespace api
} // namespace esphome

View File

@ -22,22 +22,40 @@ namespace api {
static const char *const TAG = "api"; static const char *const TAG = "api";
// APIServer // APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
APIServer::APIServer() { global_api_server = this; }
void APIServer::setup() { void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
this->setup_controller(); this->setup_controller();
socket_ = socket::socket_ip(SOCK_STREAM, 0);
if (socket_ == nullptr) { #ifdef USE_API_NOISE
ESP_LOGW(TAG, "Could not create socket."); uint32_t hash = 88491486UL;
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
SavedNoisePsk noise_pref_saved{};
if (this->noise_pref_.load(&noise_pref_saved)) {
ESP_LOGD(TAG, "Loaded saved Noise PSK");
this->set_noise_psk(noise_pref_saved.psk);
}
#endif
this->socket_ = socket::socket_ip(SOCK_STREAM, 0);
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed(); this->mark_failed();
return; return;
} }
int enable = 1; int enable = 1;
int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue // we can still continue
} }
err = socket_->setblocking(false); err = this->socket_->setblocking(false);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed(); this->mark_failed();
@ -53,14 +71,14 @@ void APIServer::setup() {
return; return;
} }
err = socket_->bind((struct sockaddr *) &server, sl); err = this->socket_->bind((struct sockaddr *) &server, sl);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed(); this->mark_failed();
return; return;
} }
err = socket_->listen(4); err = this->socket_->listen(4);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed(); this->mark_failed();
@ -92,34 +110,45 @@ void APIServer::setup() {
} }
#endif #endif
} }
void APIServer::loop() { void APIServer::loop() {
// Accept new clients // Accept new clients
while (true) { while (true) {
struct sockaddr_storage source_addr; struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr); socklen_t addr_len = sizeof(source_addr);
auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len); auto sock = this->socket_->accept((struct sockaddr *) &source_addr, &addr_len);
if (!sock) if (!sock)
break; break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); conn->start();
} }
// Partition clients into remove and active // Process clients and remove disconnected ones in a single pass
auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), if (!this->clients_.empty()) {
[](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; }); size_t client_index = 0;
// print disconnection messages while (client_index < this->clients_.size()) {
for (auto it = new_end; it != this->clients_.end(); ++it) { auto &client = this->clients_[client_index];
this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
}
// resize vector
this->clients_.erase(new_end, this->clients_.end());
for (auto &client : this->clients_) { if (client->remove_) {
client->loop(); // Handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
}
} }
if (this->reboot_timeout_ != 0) { if (this->reboot_timeout_ != 0) {
@ -136,16 +165,22 @@ void APIServer::loop() {
} }
} }
} }
void APIServer::dump_config() { void APIServer::dump_config() {
ESP_LOGCONFIG(TAG, "API Server:"); ESP_LOGCONFIG(TAG, "API Server:");
ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->port_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Using noise encryption: YES"); ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES");
}
#else #else
ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); ESP_LOGCONFIG(TAG, " Using noise encryption: NO");
#endif #endif
} }
bool APIServer::uses_password() const { return !this->password_.empty(); } bool APIServer::uses_password() const { return !this->password_.empty(); }
bool APIServer::check_password(const std::string &password) const { bool APIServer::check_password(const std::string &password) const {
// depend only on input password length // depend only on input password length
const char *a = this->password_.c_str(); const char *a = this->password_.c_str();
@ -174,7 +209,9 @@ bool APIServer::check_password(const std::string &password) const {
return result == 0; return result == 0;
} }
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
if (obj->is_internal()) if (obj->is_internal())
@ -342,57 +379,6 @@ void APIServer::on_update(update::UpdateEntity *obj) {
} }
#endif #endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void APIServer::set_password(const std::string &password) { this->password_ = password; }
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call);
}
}
APIServer::APIServer() { global_api_server = this; }
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
});
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_;
}
uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->remove_ && client->is_authenticated())
client->send_time_request();
}
}
#endif
bool APIServer::is_connected() const { return !this->clients_.empty(); }
void APIServer::on_shutdown() {
for (auto &c : this->clients_) {
c->send_disconnect_request(DisconnectRequest());
}
delay(10);
}
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (obj->is_internal()) if (obj->is_internal())
@ -402,6 +388,96 @@ void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
} }
#endif #endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
void APIServer::set_password(const std::string &password) { this->password_ = password; }
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call);
}
}
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
});
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_;
}
uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_API_NOISE
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
auto &old_psk = this->noise_ctx_->get_psk();
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old");
return true;
}
SavedNoisePsk new_saved_psk{psk};
if (!this->noise_pref_.save(&new_saved_psk)) {
ESP_LOGW(TAG, "Failed to save Noise PSK");
return false;
}
// ensure it's written immediately
if (!global_preferences->sync()) {
ESP_LOGW(TAG, "Failed to sync preferences");
return false;
}
ESP_LOGD(TAG, "Noise PSK saved");
if (make_active) {
this->set_timeout(100, [this, psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
this->set_noise_psk(psk);
for (auto &c : this->clients_) {
c->send_disconnect_request(DisconnectRequest());
}
});
}
return true;
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->remove_ && client->is_authenticated())
client->send_time_request();
}
}
#endif
bool APIServer::is_connected() const { return !this->clients_.empty(); }
void APIServer::on_shutdown() {
for (auto &c : this->clients_) {
c->send_disconnect_request(DisconnectRequest());
}
delay(10);
}
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome
#endif #endif

View File

@ -19,6 +19,12 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
} PACKED; // NOLINT
#endif
class APIServer : public Component, public Controller { class APIServer : public Component, public Controller {
public: public:
APIServer(); APIServer();
@ -35,6 +41,7 @@ class APIServer : public Component, public Controller {
void set_reboot_timeout(uint32_t reboot_timeout); void set_reboot_timeout(uint32_t reboot_timeout);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
#endif // USE_API_NOISE #endif // USE_API_NOISE
@ -142,6 +149,7 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE #endif // USE_API_NOISE
}; };

View File

@ -20,16 +20,26 @@ class ProtoVarInt {
explicit ProtoVarInt(uint64_t value) : value_(value) {} explicit ProtoVarInt(uint64_t value) : value_(value) {}
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (consumed != nullptr) if (len == 0) {
*consumed = 0; if (consumed != nullptr)
*consumed = 0;
if (len == 0)
return {}; return {};
}
uint64_t result = 0; // Most common case: single-byte varint (values 0-127)
uint8_t bitpos = 0; if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
for (uint32_t i = 0; i < len; i++) { // General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < len; i++) {
uint8_t val = buffer[i]; uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos); result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7; bitpos += 7;
@ -40,7 +50,9 @@ class ProtoVarInt {
} }
} }
return {}; if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
} }
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
@ -71,6 +83,34 @@ class ProtoVarInt {
return static_cast<int64_t>(this->value_ >> 1); return static_cast<int64_t>(this->value_ >> 1);
} }
} }
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) { void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_; uint64_t val = this->value_;
if (val <= 0x7F) { if (val <= 0x7F) {
@ -149,6 +189,18 @@ class ProtoWriteBuffer {
void write(uint8_t value) { this->buffer_->push_back(value); } void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
/**
* Encode a field key (tag/wire type combination).
*
* @param field_id Field number (tag) in the protobuf message
* @param type Wire type value:
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
* - 1: 64-bit (fixed64, sfixed64, double)
* - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
* - 5: 32-bit (fixed32, sfixed32, float)
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & 0b111); uint32_t val = (field_id << 3) | (type & 0b111);
this->encode_varint_raw(val); this->encode_varint_raw(val);
@ -157,7 +209,7 @@ class ProtoWriteBuffer {
if (len == 0 && !force) if (len == 0 && !force)
return; return;
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string); auto *data = reinterpret_cast<const uint8_t *>(string);
this->buffer_->insert(this->buffer_->end(), data, data + len); this->buffer_->insert(this->buffer_->end(), data, data + len);
@ -171,26 +223,26 @@ class ProtoWriteBuffer {
void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
this->encode_varint_raw(value); this->encode_varint_raw(value);
} }
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value)); this->encode_varint_raw(ProtoVarInt(value));
} }
void encode_bool(uint32_t field_id, bool value, bool force = false) { void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force) if (!value && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->write(0x01); this->write(0x01);
} }
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 5); this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->write((value >> 0) & 0xFF); this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF); this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
@ -200,7 +252,7 @@ class ProtoWriteBuffer {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 5); this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
this->write((value >> 0) & 0xFF); this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF); this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
@ -254,7 +306,7 @@ class ProtoWriteBuffer {
this->encode_uint64(field_id, uvalue, force); this->encode_uint64(field_id, uvalue, force);
} }
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
size_t begin = this->buffer_->size(); size_t begin = this->buffer_->size();
value.encode(*this); value.encode(*this);
@ -276,6 +328,7 @@ class ProtoMessage {
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
virtual void encode(ProtoWriteBuffer buffer) const = 0; virtual void encode(ProtoWriteBuffer buffer) const = 0;
void decode(const uint8_t *buffer, size_t length); void decode(const uint8_t *buffer, size_t length);
virtual void calculate_size(uint32_t &total_size) const = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
@ -298,13 +351,29 @@ class ProtoService {
virtual void on_fatal_error() = 0; virtual void on_fatal_error() = 0;
virtual void on_unauthenticated_access() = 0; virtual void on_unauthenticated_access() = 0;
virtual void on_no_setup_connection() = 0; virtual void on_no_setup_connection() = 0;
virtual ProtoWriteBuffer create_buffer() = 0; /**
* Create a buffer with a reserved size.
* @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint
* to optimize memory usage and avoid reallocations during encoding.
* Implementations should aim to allocate at least this size.
* @return A ProtoWriteBuffer object with the reserved size.
*/
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size
template<class C> bool send_message_(const C &msg, uint32_t message_type) { template<class C> bool send_message_(const C &msg, uint32_t message_type) {
auto buffer = this->create_buffer(); uint32_t msg_size = 0;
msg.calculate_size(msg_size);
// Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size);
// Encode message into the buffer
msg.encode(buffer); msg.encode(buffer);
// Send the buffer
return this->send_buffer(buffer, message_type); return this->send_buffer(buffer, message_type);
} }
}; };

View File

@ -7,7 +7,7 @@
namespace esphome { namespace esphome {
namespace as7341 { namespace as7341 {
static const uint8_t AS7341_CHIP_ID = 0X09; static const uint8_t AS7341_CHIP_ID = 0x09;
static const uint8_t AS7341_CONFIG = 0x70; static const uint8_t AS7341_CONFIG = 0x70;
static const uint8_t AS7341_LED = 0x74; static const uint8_t AS7341_LED = 0x74;

View File

@ -14,11 +14,8 @@ namespace esphome {
namespace at581x { namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice { class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public: public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) { void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s; this->rf_power_switch_ = s;
s->turn_on(); s->turn_on();
@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data); bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected: protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_; int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */ int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */ int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@ -3,5 +3,6 @@ import esphome.codegen as cg
CODEOWNERS = ["@circuitsetup", "@descipher"] CODEOWNERS = ["@circuitsetup", "@descipher"]
atm90e32_ns = cg.esphome_ns.namespace("atm90e32") atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component)
CONF_ATM90E32_ID = "atm90e32_id" CONF_ATM90E32_ID = "atm90e32_id"

View File

@ -1,7 +1,7 @@
#include "atm90e32.h" #include "atm90e32.h"
#include "atm90e32_reg.h"
#include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
#include <cmath>
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {
@ -11,115 +11,84 @@ void ATM90E32Component::loop() {
if (this->get_publish_interval_flag_()) { if (this->get_publish_interval_flag_()) {
this->set_publish_interval_flag_(false); this->set_publish_interval_flag_(false);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) { if (this->phase_[phase].voltage_sensor_ != nullptr)
this->phase_[phase].voltage_ = this->get_phase_voltage_(phase); this->phase_[phase].voltage_ = this->get_phase_voltage_(phase);
}
} if (this->phase_[phase].current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_ = this->get_phase_current_(phase); this->phase_[phase].current_ = this->get_phase_current_(phase);
}
} if (this->phase_[phase].power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].active_power_ = this->get_phase_active_power_(phase); this->phase_[phase].active_power_ = this->get_phase_active_power_(phase);
}
} if (this->phase_[phase].power_factor_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase); this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase);
}
} if (this->phase_[phase].reactive_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase); this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase);
}
} if (this->phase_[phase].apparent_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) { this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase);
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr)
this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase); this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase);
}
} if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase); this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase);
}
} if (this->phase_[phase].phase_angle_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase); this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase);
}
} if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase); this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase);
}
} if (this->phase_[phase].peak_current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase); this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase);
}
} // After the local store is collected we can publish them trusting they are within +-1 hardware sampling
// After the local store in collected we can publish them trusting they are withing +-1 haardware sampling if (this->phase_[phase].voltage_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) {
this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase)); this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase));
}
} if (this->phase_[phase].current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase)); this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase));
}
} if (this->phase_[phase].power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase)); this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase));
}
} if (this->phase_[phase].power_factor_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase)); this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase));
}
} if (this->phase_[phase].reactive_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase)); this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase));
}
} if (this->phase_[phase].apparent_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) { this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase));
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
this->phase_[phase].forward_active_energy_sensor_->publish_state( this->phase_[phase].forward_active_energy_sensor_->publish_state(
this->get_local_phase_forward_active_energy_(phase)); this->get_local_phase_forward_active_energy_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_sensor_->publish_state( this->phase_[phase].reverse_active_energy_sensor_->publish_state(
this->get_local_phase_reverse_active_energy_(phase)); this->get_local_phase_reverse_active_energy_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) { if (this->phase_[phase].phase_angle_sensor_ != nullptr)
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase)); this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_sensor_->publish_state( this->phase_[phase].harmonic_active_power_sensor_->publish_state(
this->get_local_phase_harmonic_active_power_(phase)); this->get_local_phase_harmonic_active_power_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) { if (this->phase_[phase].peak_current_sensor_ != nullptr)
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase)); this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase));
}
} }
if (this->freq_sensor_ != nullptr) { if (this->freq_sensor_ != nullptr)
this->freq_sensor_->publish_state(this->get_frequency_()); this->freq_sensor_->publish_state(this->get_frequency_());
}
if (this->chip_temperature_sensor_ != nullptr) { if (this->chip_temperature_sensor_ != nullptr)
this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
}
} }
} }
@ -130,82 +99,30 @@ void ATM90E32Component::update() {
} }
this->set_publish_interval_flag_(true); this->set_publish_interval_flag_(true);
this->status_clear_warning(); this->status_clear_warning();
}
void ATM90E32Component::restore_calibrations_() { #ifdef USE_TEXT_SENSOR
if (enable_offset_calibration_) { this->check_phase_status();
this->pref_.load(&this->offset_phase_); this->check_over_current();
} this->check_freq_status();
}; #endif
void ATM90E32Component::run_offset_calibrations() {
// Run the calibrations and
// Setup voltage and current calibration offsets for PHASE A
this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE B
this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE C
this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
}
void ATM90E32Component::clear_offset_calibrations() {
// Clear the calibrations and
this->offset_phase_[PHASEA].voltage_offset_ = 0;
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = 0;
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
this->offset_phase_[PHASEB].voltage_offset_ = 0;
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = 0;
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
this->offset_phase_[PHASEC].voltage_offset_ = 0;
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = 0;
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
} }
void ATM90E32Component::setup() { void ATM90E32Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component...");
this->spi_setup(); this->spi_setup();
if (this->enable_offset_calibration_) {
uint32_t hash = fnv1_hash(App.get_friendly_name());
this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true);
this->restore_calibrations_();
}
uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0;
uint16_t low_thresh = 0;
if (line_freq_ == 60) { if (line_freq_ == 60) {
mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz
// for freq threshold registers
high_thresh = 6300; // 63.00 Hz
low_thresh = 5700; // 57.00 Hz
} else {
high_thresh = 5300; // 53.00 Hz
low_thresh = 4700; // 47.00 Hz
} }
if (current_phases_ == 2) { if (current_phases_ == 2) {
@ -216,34 +133,84 @@ void ATM90E32Component::setup() {
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
delay(6); // Wait for the minimum 5ms + 1ms delay(6); // Wait for the minimum 5ms + 1ms
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) { if (!this->validate_spi_read_(0x55AA, "setup()")) {
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
this->mark_failed(); this->mark_failed();
return; return;
} }
this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering
this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0) this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms
this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000
this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default)
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold
this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
// Setup voltage and current gain for PHASE A
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain if (this->enable_offset_calibration_) {
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain // Initialize flash storage for offset calibrations
// Setup voltage and current gain for PHASE B uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain this->restore_offset_calibrations_();
// Setup voltage and current gain for PHASE C
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain // Initialize flash storage for power offset calibrations
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_();
} else {
ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(this->voltage_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
this->write16_(this->current_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].current_offset_));
this->write16_(this->power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].active_power_offset));
this->write16_(this->reactive_power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].reactive_power_offset));
}
}
if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_();
if (this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
} else {
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
}
}
} else {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
}
}
// Sag threshold (78%)
uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f);
// Overvoltage threshold (122%)
uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f);
// Write to registers
this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
this->write16_(ATM90E32_REGISTER_OVTH, ovth);
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
} }
void ATM90E32Component::dump_config() { void ATM90E32Component::dump_config() {
@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() {
LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_); LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_);
LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_);
LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_);
LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_);
@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() {
LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_);
LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_);
LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_);
LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_); LOG_SENSOR(" ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_); LOG_SENSOR(" ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_); LOG_SENSOR(" ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_);
LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_);
LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_);
LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_);
LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_);
LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_); LOG_SENSOR(" ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_); LOG_SENSOR(" ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
} }
@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
uint8_t data[2]; uint8_t data[2];
uint16_t output; uint16_t output;
this->enable(); this->enable();
delay_microseconds_safe(10); delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty
this->write_byte(addrh); this->write_byte(addrh);
this->write_byte(addrl); this->write_byte(addrl);
this->read_array(data, 2); this->read_array(data, 2);
@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
this->write_byte16(a_register); this->write_byte16(a_register);
this->write_byte16(val); this->write_byte16(val);
this->disable(); this->disable();
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val) this->validate_spi_read_(val, "write16()");
ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val);
} }
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t
float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; } float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; }
float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; }
float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; } float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; }
float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) { float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) {
@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t
float ATM90E32Component::get_phase_voltage_(uint8_t phase) { float ATM90E32Component::get_phase_voltage_(uint8_t phase) {
const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) this->validate_spi_read_(voltage, "get_phase_voltage()");
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
return (float) voltage / 100; return (float) voltage / 100;
} }
@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) {
uint16_t voltage = 0; uint16_t voltage = 0;
for (uint8_t i = 0; i < reads; i++) { for (uint8_t i = 0; i < reads; i++) {
voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) this->validate_spi_read_(voltage, "get_phase_voltage_avg_()");
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
accumulation += voltage; accumulation += voltage;
} }
voltage = accumulation / reads; voltage = accumulation / reads;
@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
uint16_t current = 0; uint16_t current = 0;
for (uint8_t i = 0; i < reads; i++) { for (uint8_t i = 0; i < reads; i++) {
current = this->read16_(ATM90E32_REGISTER_IRMS + phase); current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) this->validate_spi_read_(current, "get_phase_current_avg_()");
ESP_LOGW(TAG, "SPI IRMS current register read error.");
accumulation += current; accumulation += current;
} }
current = accumulation / reads; current = accumulation / reads;
@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
float ATM90E32Component::get_phase_current_(uint8_t phase) { float ATM90E32Component::get_phase_current_(uint8_t phase) {
const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase); const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) this->validate_spi_read_(current, "get_phase_current_()");
ESP_LOGW(TAG, "SPI IRMS current register read error.");
return (float) current / 1000; return (float) current / 1000;
} }
@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) {
return val * 0.00032f; return val * 0.00032f;
} }
float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) {
const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase);
return val * 0.00032f;
}
float ATM90E32Component::get_phase_power_factor_(uint8_t phase) { float ATM90E32Component::get_phase_power_factor_(uint8_t phase) {
const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); // unsigned to compare to lastspidata
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor) this->validate_spi_read_(powerfactor, "get_phase_power_factor_()");
ESP_LOGW(TAG, "SPI power factor read error."); return (float) ((int16_t) powerfactor) / 1000; // make it signed again
return (float) powerfactor / 1000;
} }
float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
} else { } else {
this->phase_[phase].cumulative_forward_active_energy_ = val; this->phase_[phase].cumulative_forward_active_energy_ = val;
} }
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200); // 0.01CF resolution = 0.003125 Wh per count
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f));
} }
float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) { float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) {
const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY); const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase);
if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) { if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) {
this->phase_[phase].cumulative_reverse_active_energy_ += val; this->phase_[phase].cumulative_reverse_active_energy_ += val;
} else { } else {
this->phase_[phase].cumulative_reverse_active_energy_ = val; this->phase_[phase].cumulative_reverse_active_energy_ = val;
} }
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200); // 0.01CF resolution = 0.003125 Wh per count
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f));
} }
float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
float ATM90E32Component::get_phase_angle_(uint8_t phase) { float ATM90E32Component::get_phase_angle_(uint8_t phase) {
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
return (float) (val > 180) ? val - 360.0 : val; return (val > 180) ? (float) (val - 360.0f) : (float) val;
} }
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase); int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase);
if (!this->peak_current_signed_) if (!this->peak_current_signed_)
val = abs(val); val = std::abs(val);
// phase register * phase current gain value / 1000 * 2^13 // phase register * phase current gain value / 1000 * 2^13
return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0); return (val * this->phase_[phase].ct_gain_ / 8192000.0);
} }
float ATM90E32Component::get_frequency_() { float ATM90E32Component::get_frequency_() {
@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() {
return (float) ctemp; return (float) ctemp;
} }
uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) { void ATM90E32Component::run_gain_calibrations() {
const uint8_t num_reads = 5; if (!this->enable_gain_calibration_) {
uint64_t total_value = 0; ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
for (int i = 0; i < num_reads; ++i) { return;
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase);
total_value += measurement_value;
} }
const uint32_t average_value = total_value / num_reads;
const uint32_t shifted_value = average_value >> 7; float ref_voltages[3] = {
const uint32_t voltage_offset = ~shifted_value + 1; this->get_reference_voltage(0),
return voltage_offset & 0xFFFF; // Take the lower 16 bits this->get_reference_voltage(1),
this->get_reference_voltage(2),
};
float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
this->get_reference_current(2)};
ESP_LOGI(TAG, "[CALIBRATION] ");
ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration =========================");
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
ESP_LOGI(TAG,
"[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |");
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
for (uint8_t phase = 0; phase < 3; phase++) {
float measured_voltage = this->get_phase_voltage_avg_(phase);
float measured_current = this->get_phase_current_avg_(phase);
float ref_voltage = ref_voltages[phase];
float ref_current = ref_currents[phase];
uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]);
uint16_t current_current_gain = this->read16_(current_gain_registers[phase]);
bool did_voltage = false;
bool did_current = false;
// Voltage calibration
if (ref_voltage <= 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
phase_labels[phase]);
} else if (measured_voltage == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
phase_labels[phase]);
} else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
phase_labels[phase]);
} else {
if (new_voltage_gain >= 65535) {
ESP_LOGW(
TAG,
"[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
phase_labels[phase]);
new_voltage_gain = 65535;
}
this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
did_voltage = true;
}
}
// Current calibration
if (ref_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
phase_labels[phase]);
} else if (measured_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
phase_labels[phase]);
} else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
phase_labels[phase]);
} else {
if (new_current_gain >= 65535) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
phase_labels[phase]);
new_current_gain = 65535;
}
this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
did_current = true;
}
}
// Final row output
ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |",
'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
}
ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
this->save_gain_calibration_to_memory_();
this->write_gains_to_registers_();
this->verify_gain_writes_();
} }
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { void ATM90E32Component::save_gain_calibration_to_memory_() {
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
if (success) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
}
}
void ATM90E32Component::run_offset_calibrations() {
if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
return;
}
for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = calibrate_offset(phase, true);
int16_t current_offset = calibrate_offset(phase, false);
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
current_offset);
}
this->offset_pref_.save(&this->offset_phase_); // Save to flash
}
void ATM90E32Component::run_power_offset_calibrations() {
if (!this->enable_offset_calibration_) {
ESP_LOGW(
TAG,
"[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
return;
}
for (uint8_t phase = 0; phase < 3; ++phase) {
int16_t active_offset = calibrate_power_offset(phase, false);
int16_t reactive_offset = calibrate_power_offset(phase, true);
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
active_offset, reactive_offset);
}
this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash
}
void ATM90E32Component::write_gains_to_registers_() {
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
for (int phase = 0; phase < 3; phase++) {
this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain);
this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain);
}
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) {
// Save to runtime
this->offset_phase_[phase].voltage_offset_ = voltage_offset;
this->phase_[phase].voltage_offset_ = voltage_offset;
// Save to flash-storable struct
this->offset_phase_[phase].current_offset_ = current_offset;
this->phase_[phase].current_offset_ = current_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(voltage_offset_registers[phase], static_cast<uint16_t>(voltage_offset));
this->write16_(current_offset_registers[phase], static_cast<uint16_t>(current_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) {
// Save to runtime
this->phase_[phase].active_power_offset_ = p_offset;
this->phase_[phase].reactive_power_offset_ = q_offset;
// Save to flash-storable struct
this->power_offset_phase_[phase].active_power_offset = p_offset;
this->power_offset_phase_[phase].reactive_power_offset = q_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(this->power_offset_registers[phase], static_cast<uint16_t>(p_offset));
this->write16_(this->reactive_power_offset_registers[phase], static_cast<uint16_t>(q_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::restore_gain_calibrations_() {
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
uint16_t i_gain = this->gain_phase_[phase].current_gain;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
}
this->write_gains_to_registers_();
if (this->verify_gain_writes_()) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
}
} else {
this->using_saved_calibrations_ = false;
ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
}
}
void ATM90E32Component::restore_offset_calibrations_() {
if (this->offset_pref_.load(&this->offset_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
for (uint8_t phase = 0; phase < 3; phase++) {
auto &offset = this->offset_phase_[phase];
write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
offset.voltage_offset_, offset.current_offset_);
}
} else {
ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
}
}
void ATM90E32Component::restore_power_offset_calibrations_() {
if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
for (uint8_t phase = 0; phase < 3; ++phase) {
auto &offset = this->power_offset_phase_[phase];
write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
offset.active_power_offset, offset.reactive_power_offset);
}
} else {
ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
}
}
void ATM90E32Component::clear_gain_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values...");
for (int phase = 0; phase < 3; phase++) {
gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
}
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
this->using_saved_calibrations_ = false;
if (success) {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
for (int phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
}
} else {
ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
}
this->write_gains_to_registers_(); // Apply them to the chip immediately
}
void ATM90E32Component::clear_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_offsets_to_registers_(phase, 0, 0);
}
this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory
ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
}
void ATM90E32Component::clear_power_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_power_offsets_to_registers_(phase, 0, 0);
}
this->power_offset_pref_.save(&this->power_offset_phase_);
ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
}
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
const uint8_t num_reads = 5; const uint8_t num_reads = 5;
uint64_t total_value = 0; uint64_t total_value = 0;
for (int i = 0; i < num_reads; ++i) {
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); for (uint8_t i = 0; i < num_reads; ++i) {
total_value += measurement_value; uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase)
: this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
total_value += reading;
} }
const uint32_t average_value = total_value / num_reads; const uint32_t average_value = total_value / num_reads;
const uint32_t current_offset = ~average_value + 1; const uint32_t shifted = average_value >> 7;
return current_offset & 0xFFFF; // Take the lower 16 bits const uint32_t offset = ~shifted + 1;
return static_cast<int16_t>(offset); // Takes lower 16 bits
}
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
const uint8_t num_reads = 5;
uint64_t total_value = 0;
for (uint8_t i = 0; i < num_reads; ++i) {
uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
total_value += reading;
}
const uint32_t average_value = total_value / num_reads;
const uint32_t power_offset = ~average_value + 1;
return static_cast<int16_t>(power_offset); // Takes the lower 16 bits
}
bool ATM90E32Component::verify_gain_writes_() {
bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
uint16_t read_current = this->read16_(current_gain_registers[phase]);
if (read_voltage != this->gain_phase_[phase].voltage_gain ||
read_current != this->gain_phase_[phase].current_gain) {
ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
success = false;
}
}
return success; // Return true if all writes were successful, false otherwise
}
#ifdef USE_TEXT_SENSOR
void ATM90E32Component::check_phase_status() {
uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
for (int phase = 0; phase < 3; phase++) {
std::string status;
if (state0 & over_voltage_flags[phase])
status += "Over Voltage; ";
if (state1 & voltage_sag_flags[phase])
status += "Voltage Sag; ";
if (state1 & phase_loss_flags[phase])
status += "Phase Loss; ";
auto *sensor = this->phase_status_text_sensor_[phase];
const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
if (!status.empty()) {
status.pop_back(); // remove space
status.pop_back(); // remove semicolon
ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
if (sensor != nullptr)
sensor->publish_state(status);
} else {
if (sensor != nullptr)
sensor->publish_state("Okay");
}
}
}
void ATM90E32Component::check_freq_status() {
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
std::string freq_status;
if (state1 & ATM90E32_STATUS_S1_FREQHIST) {
freq_status = "HIGH";
} else if (state1 & ATM90E32_STATUS_S1_FREQLOST) {
freq_status = "LOW";
} else {
freq_status = "Normal";
}
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
if (this->freq_status_text_sensor_ != nullptr) {
this->freq_status_text_sensor_->publish_state(freq_status);
}
}
void ATM90E32Component::check_over_current() {
constexpr float max_current_threshold = 65.53f;
for (uint8_t phase = 0; phase < 3; phase++) {
float current_val =
this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f;
if (current_val > max_current_threshold) {
ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val);
ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2");
if (this->phase_status_text_sensor_[phase] != nullptr) {
this->phase_status_text_sensor_[phase]->publish_state("Over Current; ");
}
}
}
}
#endif
uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) {
// this assumes that 60Hz electrical systems use 120V mains,
// which is usually, but not always the case
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
float target_voltage = nominal_voltage * multiplier;
float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V
float divider = (2.0f * ugain) / 32768.0f;
float threshold = peak_01v / divider;
return static_cast<uint16_t>(threshold);
}
bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) {
uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA);
if (last != expected) {
if (context != nullptr) {
ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last);
} else {
ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last);
}
return false;
}
return true;
} }
} // namespace atm90e32 } // namespace atm90e32

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <unordered_map>
#include "atm90e32_reg.h" #include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent,
static const uint8_t PHASEA = 0; static const uint8_t PHASEA = 0;
static const uint8_t PHASEB = 1; static const uint8_t PHASEB = 1;
static const uint8_t PHASEC = 2; static const uint8_t PHASEC = 2;
const char *phase_labels[3] = {"A", "B", "C"};
// these registers are not sucessive, so we can't just do 'base + phase'
const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB,
ATM90E32_REGISTER_UGAINC};
const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB,
ATM90E32_REGISTER_IGAINC};
const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB,
ATM90E32_REGISTER_UOFFSETC};
const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB,
ATM90E32_REGISTER_IOFFSETC};
const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB,
ATM90E32_REGISTER_POFFSETC};
const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB,
ATM90E32_REGISTER_QOFFSETC};
const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST,
ATM90E32_STATUS_S0_OVPHASECST};
const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST,
ATM90E32_STATUS_S1_SAGPHASECST};
const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST,
ATM90E32_STATUS_S1_PHASELOSSCST};
void loop() override; void loop() override;
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent,
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
void set_active_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].active_power_offset = offset;
}
void set_reactive_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].reactive_power_offset = offset;
}
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) {
@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent,
void set_current_phases(int phases) { current_phases_ = phases; } void set_current_phases(int phases) { current_phases_ = phases; }
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
void run_offset_calibrations(); void run_offset_calibrations();
void run_power_offset_calibrations();
void clear_offset_calibrations(); void clear_offset_calibrations();
void clear_power_offset_calibrations();
void clear_gain_calibrations();
void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; }
uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); int16_t calibrate_offset(uint8_t phase, bool voltage);
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
void run_gain_calibrations();
#ifdef USE_NUMBER
void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif
}
bool using_saved_calibrations_ = false; // Track if stored calibrations are being used
#ifdef USE_TEXT_SENSOR
void check_phase_status();
void check_freq_status();
void check_over_current();
void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) {
this->phase_status_text_sensor_[phase] = sensor;
}
void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
#endif
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
int32_t last_periodic_millis = millis(); int32_t last_periodic_millis = millis();
protected: protected:
#ifdef USE_NUMBER
number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr};
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
#endif
uint16_t read16_(uint16_t a_register); uint16_t read16_(uint16_t a_register);
int read32_(uint16_t addr_h, uint16_t addr_l); int read32_(uint16_t addr_h, uint16_t addr_l);
void write16_(uint16_t a_register, uint16_t val); void write16_(uint16_t a_register, uint16_t val);
float get_local_phase_voltage_(uint8_t /*phase*/); float get_local_phase_voltage_(uint8_t phase);
float get_local_phase_current_(uint8_t /*phase*/); float get_local_phase_current_(uint8_t phase);
float get_local_phase_active_power_(uint8_t /*phase*/); float get_local_phase_active_power_(uint8_t phase);
float get_local_phase_reactive_power_(uint8_t /*phase*/); float get_local_phase_reactive_power_(uint8_t phase);
float get_local_phase_power_factor_(uint8_t /*phase*/); float get_local_phase_apparent_power_(uint8_t phase);
float get_local_phase_forward_active_energy_(uint8_t /*phase*/); float get_local_phase_power_factor_(uint8_t phase);
float get_local_phase_reverse_active_energy_(uint8_t /*phase*/); float get_local_phase_forward_active_energy_(uint8_t phase);
float get_local_phase_angle_(uint8_t /*phase*/); float get_local_phase_reverse_active_energy_(uint8_t phase);
float get_local_phase_harmonic_active_power_(uint8_t /*phase*/); float get_local_phase_angle_(uint8_t phase);
float get_local_phase_peak_current_(uint8_t /*phase*/); float get_local_phase_harmonic_active_power_(uint8_t phase);
float get_phase_voltage_(uint8_t /*phase*/); float get_local_phase_peak_current_(uint8_t phase);
float get_phase_voltage_avg_(uint8_t /*phase*/); float get_phase_voltage_(uint8_t phase);
float get_phase_current_(uint8_t /*phase*/); float get_phase_voltage_avg_(uint8_t phase);
float get_phase_current_avg_(uint8_t /*phase*/); float get_phase_current_(uint8_t phase);
float get_phase_active_power_(uint8_t /*phase*/); float get_phase_current_avg_(uint8_t phase);
float get_phase_reactive_power_(uint8_t /*phase*/); float get_phase_active_power_(uint8_t phase);
float get_phase_power_factor_(uint8_t /*phase*/); float get_phase_reactive_power_(uint8_t phase);
float get_phase_forward_active_energy_(uint8_t /*phase*/); float get_phase_apparent_power_(uint8_t phase);
float get_phase_reverse_active_energy_(uint8_t /*phase*/); float get_phase_power_factor_(uint8_t phase);
float get_phase_angle_(uint8_t /*phase*/); float get_phase_forward_active_energy_(uint8_t phase);
float get_phase_harmonic_active_power_(uint8_t /*phase*/); float get_phase_reverse_active_energy_(uint8_t phase);
float get_phase_peak_current_(uint8_t /*phase*/); float get_phase_angle_(uint8_t phase);
float get_phase_harmonic_active_power_(uint8_t phase);
float get_phase_peak_current_(uint8_t phase);
float get_frequency_(); float get_frequency_();
float get_chip_temperature_(); float get_chip_temperature_();
bool get_publish_interval_flag_() { return publish_interval_flag_; }; bool get_publish_interval_flag_() { return publish_interval_flag_; };
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
void restore_calibrations_(); void restore_offset_calibrations_();
void restore_power_offset_calibrations_();
void restore_gain_calibrations_();
void save_gain_calibration_to_memory_();
void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
void write_gains_to_registers_();
bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
struct ATM90E32Phase { struct ATM90E32Phase {
uint16_t voltage_gain_{0}; uint16_t voltage_gain_{0};
uint16_t ct_gain_{0}; uint16_t ct_gain_{0};
uint16_t voltage_offset_{0}; int16_t voltage_offset_{0};
uint16_t current_offset_{0}; int16_t current_offset_{0};
int16_t active_power_offset_{0};
int16_t reactive_power_offset_{0};
float voltage_{0}; float voltage_{0};
float current_{0}; float current_{0};
float active_power_{0}; float active_power_{0};
float reactive_power_{0}; float reactive_power_{0};
float apparent_power_{0};
float power_factor_{0}; float power_factor_{0};
float forward_active_energy_{0}; float forward_active_energy_{0};
float reverse_active_energy_{0}; float reverse_active_energy_{0};
@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent,
uint32_t cumulative_reverse_active_energy_{0}; uint32_t cumulative_reverse_active_energy_{0};
} phase_[3]; } phase_[3];
struct Calibration { struct OffsetCalibration {
uint16_t voltage_offset_{0}; int16_t voltage_offset_{0};
uint16_t current_offset_{0}; int16_t current_offset_{0};
} offset_phase_[3]; } offset_phase_[3];
ESPPreferenceObject pref_; struct PowerOffsetCalibration {
int16_t active_power_offset{0};
int16_t reactive_power_offset{0};
} power_offset_phase_[3];
struct GainCalibration {
uint16_t voltage_gain{1};
uint16_t current_gain{1};
} gain_phase_[3];
ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_;
sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr};
text_sensor::TextSensor *freq_status_text_sensor_{nullptr};
#endif
sensor::Sensor *chip_temperature_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr};
uint16_t pga_gain_{0x15}; uint16_t pga_gain_{0x15};
int line_freq_{60}; int line_freq_{60};
@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent,
bool publish_interval_flag_{false}; bool publish_interval_flag_{false};
bool peak_current_signed_{false}; bool peak_current_signed_{false};
bool enable_offset_calibration_{false}; bool enable_offset_calibration_{false};
bool enable_gain_calibration_{false};
}; };
} // namespace atm90e32 } // namespace atm90e32

View File

@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E
/* POWER & P.F. REGISTERS */ /* POWER & P.F. REGISTERS */
static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P) static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Active Power Reg Base (P)
static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P)
static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q) static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Reactive Power Reg Base (Q)
static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9; // Apparent Mean Power Base (S)
static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S)
@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A Rea
static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power)
static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power)
static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power) static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9; // Lower Word Reg Base (Apparent Power)
static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power) static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power) static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power) static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power)

View File

@ -1,43 +1,95 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import button from esphome.components import button
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from .. import atm90e32_ns from .. import atm90e32_ns
from ..sensor import ATM90E32Component from ..sensor import ATM90E32Component
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration"
CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration"
CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
ATM90E32CalibrationButton = atm90e32_ns.class_( ATM90E32GainCalibrationButton = atm90e32_ns.class_(
"ATM90E32CalibrationButton", "ATM90E32GainCalibrationButton", button.Button
button.Button,
) )
ATM90E32ClearCalibrationButton = atm90e32_ns.class_( ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearCalibrationButton", "ATM90E32ClearGainCalibrationButton", button.Button
button.Button, )
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32OffsetCalibrationButton", button.Button
)
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearOffsetCalibrationButton", button.Button
)
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32PowerOffsetCalibrationButton", button.Button
)
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearPowerOffsetCalibrationButton", button.Button
) )
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
ATM90E32GainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:scale-balance",
),
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
ATM90E32ClearGainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
),
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
ATM90E32CalibrationButton, ATM90E32OffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE, icon=ICON_SCALE,
), ),
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearCalibrationButton, ATM90E32ClearOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP, icon="mdi:delete",
),
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32PowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearPowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
), ),
} }
async def to_code(config): async def to_code(config):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
b = await button.new_button(run_gain)
await cg.register_parented(b, parent)
if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
b = await button.new_button(clear_gain)
await cg.register_parented(b, parent)
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
b = await button.new_button(run_offset) b = await button.new_button(run_offset)
await cg.register_parented(b, parent) await cg.register_parented(b, parent)
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
b = await button.new_button(clear_offset) b = await button.new_button(clear_offset)
await cg.register_parented(b, parent) await cg.register_parented(b, parent)
if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
b = await button.new_button(run_power)
await cg.register_parented(b, parent)
if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
b = await button.new_button(clear_power)
await cg.register_parented(b, parent)

View File

@ -1,4 +1,5 @@
#include "atm90e32_button.h" #include "atm90e32_button.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@ -6,15 +7,73 @@ namespace atm90e32 {
static const char *const TAG = "atm90e32.button"; static const char *const TAG = "atm90e32.button";
void ATM90E32CalibrationButton::press_action() { void ATM90E32GainCalibrationButton::press_action() {
ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG,
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
this->parent_->run_gain_calibrations();
}
void ATM90E32ClearGainCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_gain_calibrations();
}
void ATM90E32OffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
"these values");
this->parent_->run_offset_calibrations(); this->parent_->run_offset_calibrations();
} }
void ATM90E32ClearCalibrationButton::press_action() { void ATM90E32ClearOffsetCalibrationButton::press_action() {
ESP_LOGI(TAG, "Offset calibrations cleared."); if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_offset_calibrations(); this->parent_->clear_offset_calibrations();
} }
void ATM90E32PowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
"file to save these values");
this->parent_->run_power_offset_calibrations();
}
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_power_offset_calibrations();
}
} // namespace atm90e32 } // namespace atm90e32
} // namespace esphome } // namespace esphome

View File

@ -7,17 +7,49 @@
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {
class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> { class ATM90E32GainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public: public:
ATM90E32CalibrationButton() = default; ATM90E32GainCalibrationButton() = default;
protected: protected:
void press_action() override; void press_action() override;
}; };
class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> { class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public: public:
ATM90E32ClearCalibrationButton() = default; ATM90E32ClearGainCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32OffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32OffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32PowerOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearPowerOffsetCalibrationButton() = default;
protected: protected:
void press_action() override; void press_action() override;

View File

@ -0,0 +1,130 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_MODE,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_REFERENCE_VOLTAGE,
CONF_STEP,
ENTITY_CATEGORY_CONFIG,
UNIT_AMPERE,
UNIT_VOLT,
)
from .. import atm90e32_ns
from ..sensor import ATM90E32Component
ATM90E32Number = atm90e32_ns.class_(
"ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component)
)
CONF_REFERENCE_CURRENT = "reference_current"
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_MODE, default="box"): cv.string,
cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_,
cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_,
cv.Optional(CONF_STEP, default=0.1): cv.float_,
}
).extend(
number.number_schema(
class_=ATM90E32Number,
unit_of_measurement=UNIT_VOLT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:power-plug",
)
)
)
REFERENCE_CURRENT_PHASE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_MODE, default="box"): cv.string,
cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_,
cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_,
cv.Optional(CONF_STEP, default=0.1): cv.float_,
}
).extend(
number.number_schema(
class_=ATM90E32Number,
unit_of_measurement=UNIT_AMPERE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:home-lightning-bolt",
)
)
)
REFERENCE_VOLTAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA,
}
)
REFERENCE_CURRENT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA,
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA,
cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA,
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE):
voltage_objs = [None, None, None]
for i, key in enumerate(PHASE_KEYS):
if validated := voltage_cfg.get(key):
obj = await number.new_number(
validated,
min_value=validated["min_value"],
max_value=validated["max_value"],
step=validated["step"],
)
await cg.register_parented(obj, parent)
voltage_objs[i] = obj
# Inherit from A → B/C if only A defined
if voltage_objs[0] is not None:
for i in range(3):
if voltage_objs[i] is None:
voltage_objs[i] = voltage_objs[0]
for i, obj in enumerate(voltage_objs):
if obj is not None:
cg.add(parent.set_reference_voltage(i, obj))
if current_cfg := config.get(CONF_REFERENCE_CURRENT):
for i, key in enumerate(PHASE_KEYS):
if validated := current_cfg.get(key):
obj = await number.new_number(
validated,
min_value=validated["min_value"],
max_value=validated["max_value"],
step=validated["step"],
)
await cg.register_parented(obj, parent)
cg.add(parent.set_reference_current(i, obj))

View File

@ -0,0 +1,16 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/atm90e32/atm90e32.h"
#include "esphome/components/number/number.h"
namespace esphome {
namespace atm90e32 {
class ATM90E32Number : public number::Number, public Parented<ATM90E32Component> {
public:
void control(float value) override { this->publish_state(value); }
};
} // namespace atm90e32
} // namespace esphome

View File

@ -33,6 +33,7 @@ from esphome.const import (
UNIT_DEGREES, UNIT_DEGREES,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_VOLT, UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE, UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases" CONF_CURRENT_PHASES = "current_phases"
CONF_GAIN_VOLTAGE = "gain_voltage" CONF_GAIN_VOLTAGE = "gain_voltage"
CONF_GAIN_CT = "gain_ct" CONF_GAIN_CT = "gain_ct"
CONF_OFFSET_VOLTAGE = "offset_voltage"
CONF_OFFSET_CURRENT = "offset_current"
CONF_OFFSET_ACTIVE_POWER = "offset_active_power"
CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power"
CONF_HARMONIC_POWER = "harmonic_power" CONF_HARMONIC_POWER = "harmonic_power"
CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT = "peak_current"
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration"
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
UNIT_DEG = "degrees" UNIT_DEG = "degrees"
LINE_FREQS = { LINE_FREQS = {
"50HZ": 50, "50HZ": 50,
@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
icon=ICON_LIGHTBULB, icon=ICON_LIGHTBULB,
accuracy_decimals=2, accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT, unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=2, accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
), ),
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_,
cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_,
cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_,
cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_,
} }
) )
@ -164,9 +177,10 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum(
CURRENT_PHASES, upper=True CURRENT_PHASES, upper=True
), ),
cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean,
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@ -185,6 +199,10 @@ async def to_code(config):
conf = config[phase] conf = config[phase]
cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE]))
cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT]))
cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER]))
cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER]))
if voltage_config := conf.get(CONF_VOLTAGE): if voltage_config := conf.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config) sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(i, sens)) cg.add(var.set_voltage_sensor(i, sens))
@ -218,16 +236,15 @@ async def to_code(config):
if peak_current_config := conf.get(CONF_PEAK_CURRENT): if peak_current_config := conf.get(CONF_PEAK_CURRENT):
sens = await sensor.new_sensor(peak_current_config) sens = await sensor.new_sensor(peak_current_config)
cg.add(var.set_peak_current_sensor(i, sens)) cg.add(var.set_peak_current_sensor(i, sens))
if frequency_config := config.get(CONF_FREQUENCY): if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config) sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens)) cg.add(var.set_freq_sensor(sens))
if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE): if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
sens = await sensor.new_sensor(chip_temperature_config) sens = await sensor.new_sensor(chip_temperature_config)
cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_chip_temperature_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))
cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION]))

View File

@ -0,0 +1,48 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
from ..sensor import ATM90E32Component
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
PHASE_STATUS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(ATM90E32Component),
cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA,
cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema(
icon="mdi:lightbulb-alert"
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if phase_cfg := config.get(CONF_PHASE_STATUS):
for i, key in enumerate(PHASE_KEYS):
if sub_phase_cfg := phase_cfg.get(key):
sens = await text_sensor.new_text_sensor(sub_phase_cfg)
cg.add(parent.set_phase_status_text_sensor(i, sens))
if freq_status_config := config.get(CONF_FREQUENCY_STATUS):
sens = await text_sensor.new_text_sensor(freq_status_config)
cg.add(parent.set_freq_status_text_sensor(sens))

View File

@ -37,29 +37,32 @@ AUDIO_COMPONENT_SCHEMA = cv.Schema(
) )
_UNDEF = object()
def set_stream_limits( def set_stream_limits(
min_bits_per_sample: int = _UNDEF, min_bits_per_sample: int = cv.UNDEFINED,
max_bits_per_sample: int = _UNDEF, max_bits_per_sample: int = cv.UNDEFINED,
min_channels: int = _UNDEF, min_channels: int = cv.UNDEFINED,
max_channels: int = _UNDEF, max_channels: int = cv.UNDEFINED,
min_sample_rate: int = _UNDEF, min_sample_rate: int = cv.UNDEFINED,
max_sample_rate: int = _UNDEF, max_sample_rate: int = cv.UNDEFINED,
): ):
"""Sets the limits for the audio stream that audio component can handle
When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive.
When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send.
"""
def set_limits_in_config(config): def set_limits_in_config(config):
if min_bits_per_sample is not _UNDEF: if min_bits_per_sample is not cv.UNDEFINED:
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
if max_bits_per_sample is not _UNDEF: if max_bits_per_sample is not cv.UNDEFINED:
config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample
if min_channels is not _UNDEF: if min_channels is not cv.UNDEFINED:
config[CONF_MIN_CHANNELS] = min_channels config[CONF_MIN_CHANNELS] = min_channels
if max_channels is not _UNDEF: if max_channels is not cv.UNDEFINED:
config[CONF_MAX_CHANNELS] = max_channels config[CONF_MAX_CHANNELS] = max_channels
if min_sample_rate is not _UNDEF: if min_sample_rate is not cv.UNDEFINED:
config[CONF_MIN_SAMPLE_RATE] = min_sample_rate config[CONF_MIN_SAMPLE_RATE] = min_sample_rate
if max_sample_rate is not _UNDEF: if max_sample_rate is not cv.UNDEFINED:
config[CONF_MAX_SAMPLE_RATE] = max_sample_rate config[CONF_MAX_SAMPLE_RATE] = max_sample_rate
return set_limits_in_config return set_limits_in_config
@ -69,43 +72,87 @@ def final_validate_audio_schema(
name: str, name: str,
*, *,
audio_device: str, audio_device: str,
bits_per_sample: int, bits_per_sample: int = cv.UNDEFINED,
channels: int, channels: int = cv.UNDEFINED,
sample_rate: int, sample_rate: int = cv.UNDEFINED,
enabled_channels: list[int] = cv.UNDEFINED,
audio_device_issue: bool = False,
): ):
"""Validates audio compatibility when passed between different components.
The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings
- If audio_device_issue is True, then the error message indicates the user should adjust the AUDIO_COMPONENT_SCHEMA derived component's configuration to match the values passed to this function
- If audio_device_issue is False, then the error message indicates the user should adjust the configuration of the component calling this function, as it falls out of the valid stream limits
Args:
name (str): Friendly name of the component calling this function with an audio component to validate
audio_device (str): The configuration parameter name that contains the ID of an AUDIO_COMPONENT_SCHEMA derived component to validate against
bits_per_sample (int, optional): The desired bits per sample
channels (int, optional): The desired number of channels
sample_rate (int, optional): The desired sample rate
enabled_channels (list[int], optional): The desired enabled channels
audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False.
"""
def validate_audio_compatiblity(audio_config): def validate_audio_compatiblity(audio_config):
audio_schema = {} audio_schema = {}
try: if bits_per_sample is not cv.UNDEFINED:
cv.int_range( try:
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), cv.int_range(
max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE), min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
)(bits_per_sample) max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
except cv.Invalid as exc: )(bits_per_sample)
raise cv.Invalid( except cv.Invalid as exc:
f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}" if audio_device_issue:
) from exc error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {bits_per_sample} bits per sample."
else:
error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
raise cv.Invalid(error_string) from exc
try: if channels is not cv.UNDEFINED:
cv.int_range( try:
min=audio_config.get(CONF_MIN_CHANNELS), cv.int_range(
max=audio_config.get(CONF_MAX_CHANNELS), min=audio_config.get(CONF_MIN_CHANNELS),
)(channels) max=audio_config.get(CONF_MAX_CHANNELS),
except cv.Invalid as exc: )(channels)
raise cv.Invalid( except cv.Invalid as exc:
f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}" if audio_device_issue:
) from exc error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {channels} channels."
else:
error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
raise cv.Invalid(error_string) from exc
try: if sample_rate is not cv.UNDEFINED:
cv.int_range( try:
min=audio_config.get(CONF_MIN_SAMPLE_RATE), cv.int_range(
max=audio_config.get(CONF_MAX_SAMPLE_RATE), min=audio_config.get(CONF_MIN_SAMPLE_RATE),
)(sample_rate) max=audio_config.get(CONF_MAX_SAMPLE_RATE),
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config) )(sample_rate)
except cv.Invalid as exc: except cv.Invalid as exc:
raise cv.Invalid( if audio_device_issue:
f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}" error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires a {sample_rate} sample rate."
) from exc else:
error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
raise cv.Invalid(error_string) from exc
if enabled_channels is not cv.UNDEFINED:
for channel in enabled_channels:
try:
# Channels are 0-indexed
cv.int_range(
min=0,
max=audio_config.get(CONF_MAX_CHANNELS) - 1,
)(channel)
except cv.Invalid as exc:
if audio_device_issue:
error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires channel {channel}."
else:
error_string = f"Invalid configuration for the {name} component. Enabled channel {channel} {str(exc)}"
raise cv.Invalid(error_string) from exc
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
return cv.Schema( return cv.Schema(
{ {
@ -118,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config): async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.3") cg.add_library("esphome/esp-audio-libs", "1.1.4")

View File

@ -135,5 +135,53 @@ const char *audio_file_type_to_string(AudioFileType file_type);
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale); size_t samples_to_scale);
/// @brief Unpacks a quantized audio sample into a Q31 fixed-point number.
/// @param data Pointer to uint8_t array containing the audio sample
/// @param bytes_per_sample The number of bytes per sample
/// @return Q31 sample
inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_sample) {
int32_t sample = 0;
if (bytes_per_sample == 1) {
sample |= data[0] << 24;
} else if (bytes_per_sample == 2) {
sample |= data[0] << 16;
sample |= data[1] << 24;
} else if (bytes_per_sample == 3) {
sample |= data[0] << 8;
sample |= data[1] << 16;
sample |= data[2] << 24;
} else if (bytes_per_sample == 4) {
sample |= data[0];
sample |= data[1] << 8;
sample |= data[2] << 16;
sample |= data[3] << 24;
}
return sample;
}
/// @brief Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample.
/// Packs the most significant bits - no dithering is applied.
/// @param sample Q31 fixed-point number to pack
/// @param data Pointer to data array to store
/// @param bytes_per_sample The audio data's bytes per sample
inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample) {
if (bytes_per_sample == 1) {
data[0] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 2) {
data[0] = static_cast<uint8_t>(sample >> 16);
data[1] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 3) {
data[0] = static_cast<uint8_t>(sample >> 8);
data[1] = static_cast<uint8_t>(sample >> 16);
data[2] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 4) {
data[0] = static_cast<uint8_t>(sample);
data[1] = static_cast<uint8_t>(sample >> 8);
data[2] = static_cast<uint8_t>(sample >> 16);
data[3] = static_cast<uint8_t>(sample >> 24);
}
}
} // namespace audio } // namespace audio
} // namespace esphome } // namespace esphome

View File

@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
bytes_available_before_processing = this->input_transfer_buffer_->available(); bytes_available_before_processing = this->input_transfer_buffer_->available();
if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) { if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data // Failed to decode in last attempt and there is no new data
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {

View File

@ -4,6 +4,8 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include <cstring>
namespace esphome { namespace esphome {
namespace audio { namespace audio {

View File

@ -6,6 +6,7 @@
#include "audio_transfer_buffer.h" #include "audio_transfer_buffer.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/ring_buffer.h" #include "esphome/core/ring_buffer.h"
#ifdef USE_SPEAKER #ifdef USE_SPEAKER

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@bazuchan"] CODEOWNERS = ["@bazuchan"]
@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"]
ballu_ns = cg.esphome_ns.namespace("ballu") ballu_ns = cg.esphome_ns.namespace("ballu")
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
{
cv.GenerateID(): cv.declare_id(BalluClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -9,7 +9,6 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION, CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR, CONF_HUMIDITY_SENSOR,
CONF_ID,
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_SENSOR, CONF_SENSOR,
) )
@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com
BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig") BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig")
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(BangBangClimate)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
} }
).extend(cv.COMPONENT_SCHEMA), )
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION),
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
sens = await cg.get_variable(config[CONF_SENSOR]) sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))

View File

@ -3,6 +3,7 @@
#include "bedjet_hub.h" #include "bedjet_hub.h"
#include "bedjet_child.h" #include "bedjet_child.h"
#include "bedjet_const.h" #include "bedjet_const.h"
#include "esphome/core/application.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {

View File

@ -1,11 +1,8 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_HEAT_MODE, CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT, CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE, CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID, CONF_TIME_ID,
@ -13,7 +10,6 @@ from esphome.const import (
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"] CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["bedjet"] DEPENDENCIES = ["bedjet"]
@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = {
} }
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(BedJetClimate)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BedJetClimate),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True BEDJET_HEAT_MODES, lower=True
), ),
@ -63,9 +59,8 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))

View File

@ -1,31 +1,22 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"] CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["bedjet"] DEPENDENCIES = ["bedjet"]
BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
fan.FAN_SCHEMA.extend( fan.fan_schema(BedJetFan)
{
cv.GenerateID(): cv.declare_id(BedJetFan),
}
)
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(BEDJET_CLIENT_SCHEMA) .extend(BEDJET_CLIENT_SCHEMA)
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await fan.new_fan(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await fan.register_fan(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)

View File

@ -1,31 +1,28 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan, output from esphome.components import fan, output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT
CONF_DIRECTION_OUTPUT,
CONF_OSCILLATION_OUTPUT,
CONF_OUTPUT,
CONF_OUTPUT_ID,
)
from .. import binary_ns from .. import binary_ns
BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( CONFIG_SCHEMA = (
{ fan.fan_schema(BinaryFan)
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), .extend(
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), {
cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
} cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) var = await fan.new_fan(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await fan.register_fan(var, config)
output_ = await cg.get_variable(config[CONF_OUTPUT]) output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_)) cg.add(var.set_output(output_))

View File

@ -386,7 +386,7 @@ def validate_click_timing(value):
return value return value
BINARY_SENSOR_SCHEMA = ( _BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA)
.extend( .extend(
@ -458,19 +458,17 @@ BINARY_SENSOR_SCHEMA = (
) )
) )
_UNDEF = object()
def binary_sensor_schema( def binary_sensor_schema(
class_: MockObjClass = _UNDEF, class_: MockObjClass = cv.UNDEFINED,
*, *,
icon: str = _UNDEF, icon: str = cv.UNDEFINED,
entity_category: str = _UNDEF, entity_category: str = cv.UNDEFINED,
device_class: str = _UNDEF, device_class: str = cv.UNDEFINED,
) -> cv.Schema: ) -> cv.Schema:
schema = {} schema = {}
if class_ is not _UNDEF: if class_ is not cv.UNDEFINED:
# Not cv.optional # Not cv.optional
schema[cv.GenerateID()] = cv.declare_id(class_) schema[cv.GenerateID()] = cv.declare_id(class_)
@ -479,10 +477,15 @@ def binary_sensor_schema(
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_DEVICE_CLASS, device_class, validate_device_class),
]: ]:
if default is not _UNDEF: if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator schema[cv.Optional(key, default=default)] = validator
return BINARY_SENSOR_SCHEMA.extend(schema) return _BINARY_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):

View File

@ -45,7 +45,7 @@ static const uint8_t BL0906_WRITE_COMMAND = 0xCA;
static const uint8_t BL0906_V_RMS = 0x16; static const uint8_t BL0906_V_RMS = 0x16;
// Total power // Total power
static const uint8_t BL0906_WATT_SUM = 0X2C; static const uint8_t BL0906_WATT_SUM = 0x2C;
// Current1~6 // Current1~6
static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1 static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1
@ -56,29 +56,29 @@ static const uint8_t BL0906_I_5_RMS = 0x13;
static const uint8_t BL0906_I_6_RMS = 0x14; // current_6 static const uint8_t BL0906_I_6_RMS = 0x14; // current_6
// Power1~6 // Power1~6
static const uint8_t BL0906_WATT_1 = 0X23; // power_1 static const uint8_t BL0906_WATT_1 = 0x23; // power_1
static const uint8_t BL0906_WATT_2 = 0X24; static const uint8_t BL0906_WATT_2 = 0x24;
static const uint8_t BL0906_WATT_3 = 0X25; static const uint8_t BL0906_WATT_3 = 0x25;
static const uint8_t BL0906_WATT_4 = 0X26; static const uint8_t BL0906_WATT_4 = 0x26;
static const uint8_t BL0906_WATT_5 = 0X29; static const uint8_t BL0906_WATT_5 = 0x29;
static const uint8_t BL0906_WATT_6 = 0X2A; // power_6 static const uint8_t BL0906_WATT_6 = 0x2A; // power_6
// Active pulse count, unsigned // Active pulse count, unsigned
static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1 static const uint8_t BL0906_CF_1_CNT = 0x30; // Channel_1
static const uint8_t BL0906_CF_2_CNT = 0X31; static const uint8_t BL0906_CF_2_CNT = 0x31;
static const uint8_t BL0906_CF_3_CNT = 0X32; static const uint8_t BL0906_CF_3_CNT = 0x32;
static const uint8_t BL0906_CF_4_CNT = 0X33; static const uint8_t BL0906_CF_4_CNT = 0x33;
static const uint8_t BL0906_CF_5_CNT = 0X36; static const uint8_t BL0906_CF_5_CNT = 0x36;
static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6 static const uint8_t BL0906_CF_6_CNT = 0x37; // Channel_6
// Total active pulse count, unsigned // Total active pulse count, unsigned
static const uint8_t BL0906_CF_SUM_CNT = 0X39; static const uint8_t BL0906_CF_SUM_CNT = 0x39;
// Voltage frequency cycle // Voltage frequency cycle
static const uint8_t BL0906_FREQUENCY = 0X4E; static const uint8_t BL0906_FREQUENCY = 0x4E;
// Internal temperature // Internal temperature
static const uint8_t BL0906_TEMPERATURE = 0X5E; static const uint8_t BL0906_TEMPERATURE = 0x5E;
// Calibration register // Calibration register
// RMS gain adjustment register // RMS gain adjustment register

View File

@ -4,7 +4,6 @@ from esphome.components import ble_client, esp32_ble_tracker, text_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHARACTERISTIC_UUID, CONF_CHARACTERISTIC_UUID,
CONF_ID,
CONF_NOTIFY, CONF_NOTIFY,
CONF_SERVICE_UUID, CONF_SERVICE_UUID,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
@ -32,9 +31,9 @@ BLETextSensorNotifyTrigger = ble_client_ns.class_(
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
text_sensor.TEXT_SENSOR_SCHEMA.extend( text_sensor.text_sensor_schema(BLETextSensor)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BLETextSensor),
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
@ -54,7 +53,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await text_sensor.new_text_sensor(config)
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add( cg.add(
var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
@ -101,7 +100,6 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
cg.add(var.set_enable_notify(config[CONF_NOTIFY])) cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
await text_sensor.register_text_sensor(var, config)
for conf in config.get(CONF_ON_NOTIFY, []): for conf in config.get(CONF_ON_NOTIFY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await ble_client.register_ble_node(trigger, config) await ble_client.register_ble_node(trigger, config)

View File

@ -73,9 +73,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->read.handle; resp.handle = param->read.handle;
resp.data.reserve(param->read.value_len); resp.data.reserve(param->read.value_len);
for (uint16_t i = 0; i < param->read.value_len; i++) { // Use bulk insert instead of individual push_backs
resp.data.push_back(param->read.value[i]); resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
}
this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp);
break; break;
} }
@ -127,9 +126,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->notify.handle; resp.handle = param->notify.handle;
resp.data.reserve(param->notify.value_len); resp.data.reserve(param->notify.value_len);
for (uint16_t i = 0; i < param->notify.value_len; i++) { // Use bulk insert instead of individual push_backs
resp.data.push_back(param->notify.value[i]); resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
}
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp);
break; break;
} }

View File

@ -2,6 +2,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -25,6 +26,22 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
void BluetoothProxy::setup() {
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
}
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
api::BluetoothScannerStateResponse resp;
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
this->api_connection_->send_bluetooth_scanner_state_response(resp);
}
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_)
return false; return false;
@ -35,33 +52,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
return true; return true;
} }
static constexpr size_t FLUSH_BATCH_SIZE = 8;
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
return batch_buffer;
}
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false; return false;
api::BluetoothLERawAdvertisementsResponse resp; // Get the batch buffer reference
auto &batch_buffer = get_batch_buffer();
// Reserve additional capacity if needed
size_t new_size = batch_buffer.size() + count;
if (batch_buffer.capacity() < new_size) {
batch_buffer.reserve(new_size);
}
// Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
auto &result = advertisements[i]; auto &result = advertisements[i];
api::BluetoothLERawAdvertisement adv; uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back();
auto &adv = batch_buffer.back();
adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi; adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type; adv.address_type = result.ble_addr_type;
adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
uint8_t length = result.adv_data_len + result.scan_rsp_len; ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
adv.data.reserve(length);
for (uint16_t i = 0; i < length; i++) {
adv.data.push_back(result.ble_adv[i]);
}
resp.advertisements.push_back(std::move(adv));
ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
} }
ESP_LOGV(TAG, "Proxying %d packets", count);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); // Only send if we've accumulated a good batch size to maximize batching efficiency
// https://github.com/esphome/backlog/issues/21
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
this->flush_pending_advertisements();
}
return true; return true;
} }
void BluetoothProxy::flush_pending_advertisements() {
auto &batch_buffer = get_batch_buffer();
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
api::BluetoothLERawAdvertisementsResponse resp;
resp.advertisements.swap(batch_buffer);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
}
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp; api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64(); resp.address = device.address_uint64();
@ -69,21 +113,34 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
if (!device.get_name().empty()) if (!device.get_name().empty())
resp.name = device.get_name(); resp.name = device.get_name();
resp.rssi = device.get_rssi(); resp.rssi = device.get_rssi();
for (auto uuid : device.get_service_uuids()) {
resp.service_uuids.push_back(uuid.to_string()); // Pre-allocate vectors based on known sizes
auto service_uuids = device.get_service_uuids();
resp.service_uuids.reserve(service_uuids.size());
for (auto &uuid : service_uuids) {
resp.service_uuids.emplace_back(uuid.to_string());
} }
for (auto &data : device.get_service_datas()) {
api::BluetoothServiceData service_data; // Pre-allocate service data vector
auto service_datas = device.get_service_datas();
resp.service_data.reserve(service_datas.size());
for (auto &data : service_datas) {
resp.service_data.emplace_back();
auto &service_data = resp.service_data.back();
service_data.uuid = data.uuid.to_string(); service_data.uuid = data.uuid.to_string();
service_data.data.assign(data.data.begin(), data.data.end()); service_data.data.assign(data.data.begin(), data.data.end());
resp.service_data.push_back(std::move(service_data));
} }
for (auto &data : device.get_manufacturer_datas()) {
api::BluetoothServiceData manufacturer_data; // Pre-allocate manufacturer data vector
auto manufacturer_datas = device.get_manufacturer_datas();
resp.manufacturer_data.reserve(manufacturer_datas.size());
for (auto &data : manufacturer_datas) {
resp.manufacturer_data.emplace_back();
auto &manufacturer_data = resp.manufacturer_data.back();
manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.uuid = data.uuid.to_string();
manufacturer_data.data.assign(data.data.begin(), data.data.end()); manufacturer_data.data.assign(data.data.begin(), data.data.end());
resp.manufacturer_data.push_back(std::move(manufacturer_data));
} }
this->api_connection_->send_bluetooth_le_advertisement(resp); this->api_connection_->send_bluetooth_le_advertisement(resp);
} }
@ -117,6 +174,18 @@ void BluetoothProxy::loop() {
} }
return; return;
} }
// Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {
this->flush_pending_advertisements();
last_flush_time = now;
}
}
for (auto *connection : this->connections_) { for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) { if (connection->send_service_ == connection->service_count_) {
connection->send_service_ = DONE_SENDING_SERVICES; connection->send_service_ = DONE_SENDING_SERVICES;
@ -145,11 +214,27 @@ void BluetoothProxy::loop() {
} }
api::BluetoothGATTGetServicesResponse resp; api::BluetoothGATTGetServicesResponse resp;
resp.address = connection->get_address(); resp.address = connection->get_address();
resp.services.reserve(1); // Always one service per response in this implementation
api::BluetoothGATTService service_resp; api::BluetoothGATTService service_resp;
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
service_resp.handle = service_result.start_handle; service_resp.handle = service_result.start_handle;
uint16_t char_offset = 0; uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result; esp_gattc_char_elem_t char_result;
// Get the number of characteristics directly with one call
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
// Only reserve if we successfully got a count
service_resp.characteristics.reserve(total_char_count);
} else if (char_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
connection->address_str().c_str(), char_count_status);
}
// Now process characteristics
while (true) { // characteristics while (true) { // characteristics
uint16_t char_count = 1; uint16_t char_count = 1;
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
@ -171,6 +256,23 @@ void BluetoothProxy::loop() {
characteristic_resp.handle = char_result.char_handle; characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties; characteristic_resp.properties = char_result.properties;
char_offset++; char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status =
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
// Only reserve if we successfully got a count
characteristic_resp.descriptors.reserve(total_desc_count);
} else if (desc_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
desc_count_status);
}
// Now process descriptors
uint16_t desc_offset = 0; uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result; esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors while (true) { // descriptors
@ -453,6 +555,8 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
this->api_connection_ = api_connection; this->api_connection_ = api_connection;
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
this->parent_->recalculate_advertisement_parser_types(); this->parent_->recalculate_advertisement_parser_types();
this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
} }
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
@ -525,6 +629,17 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
this->api_connection_->send_bluetooth_device_unpairing_response(call); this->api_connection_->send_bluetooth_device_unpairing_response(call);
} }
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
if (this->parent_->get_scan_active() == active) {
return;
}
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
this->parent_->set_scan_active(active);
this->parent_->stop_scan();
this->parent_->set_scan_continuous(
true); // Set this to true to automatically start scanning again when it has cleaned up.
}
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace bluetooth_proxy } // namespace bluetooth_proxy

View File

@ -41,6 +41,7 @@ enum BluetoothProxyFeature : uint32_t {
FEATURE_PAIRING = 1 << 3, FEATURE_PAIRING = 1 << 3,
FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_CACHE_CLEARING = 1 << 4,
FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_RAW_ADVERTISEMENTS = 1 << 5,
FEATURE_STATE_AND_MODE = 1 << 6,
}; };
enum BluetoothProxySubscriptionFlag : uint32_t { enum BluetoothProxySubscriptionFlag : uint32_t {
@ -53,7 +54,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
void dump_config() override; void dump_config() override;
void setup() override;
void loop() override; void loop() override;
void flush_pending_advertisements();
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) { void register_connection(BluetoothConnection *connection) {
@ -84,6 +87,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK);
void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK);
void bluetooth_scanner_set_mode(bool active);
static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) {
bd_addr[0] = (address >> 40) & 0xff; bd_addr[0] = (address >> 40) & 0xff;
bd_addr[1] = (address >> 32) & 0xff; bd_addr[1] = (address >> 32) & 0xff;
@ -107,6 +112,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
uint32_t flags = 0; uint32_t flags = 0;
flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN;
flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS;
flags |= BluetoothProxyFeature::FEATURE_STATE_AND_MODE;
if (this->active_) { if (this->active_) {
flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS;
flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING;
@ -124,6 +130,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
protected: protected:
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);

View File

@ -44,7 +44,7 @@ ButtonPressTrigger = button_ns.class_(
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
BUTTON_SCHEMA = ( _BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
@ -60,15 +60,13 @@ BUTTON_SCHEMA = (
) )
) )
_UNDEF = object()
def button_schema( def button_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
icon: str = _UNDEF, icon: str = cv.UNDEFINED,
entity_category: str = _UNDEF, entity_category: str = cv.UNDEFINED,
device_class: str = _UNDEF, device_class: str = cv.UNDEFINED,
) -> cv.Schema: ) -> cv.Schema:
schema = {cv.GenerateID(): cv.declare_id(class_)} schema = {cv.GenerateID(): cv.declare_id(class_)}
@ -77,10 +75,15 @@ def button_schema(
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_DEVICE_CLASS, device_class, validate_device_class),
]: ]:
if default is not _UNDEF: if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator schema[cv.Optional(key, default=default)] = validator
return BUTTON_SCHEMA.extend(schema) return _BUTTON_SCHEMA.extend(schema)
# Remove before 2025.11.0
BUTTON_SCHEMA = button_schema(Button)
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config): async def setup_button_core_(var, config):

View File

@ -86,6 +86,9 @@ void Canbus::loop() {
data.push_back(can_message.data[i]); data.push_back(can_message.data[i]);
} }
this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
data);
// fire all triggers // fire all triggers
for (auto *trigger : this->triggers_) { for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&

View File

@ -81,6 +81,20 @@ class Canbus : public Component {
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void add_trigger(CanbusTrigger *trigger); void add_trigger(CanbusTrigger *trigger);
/**
* Add a callback to be called when a CAN message is received. All received messages
* are passed to the callback without filtering.
*
* The callback function receives:
* - can_id of the received data
* - extended_id True if the can_id is an extended id
* - rtr If this is a remote transmission request
* - data The message data
*/
void add_callback(
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
this->callback_manager_.add(std::move(callback));
}
protected: protected:
template<typename... Ts> friend class CanbusSendAction; template<typename... Ts> friend class CanbusSendAction;
@ -88,6 +102,8 @@ class Canbus : public Component {
uint32_t can_id_; uint32_t can_id_;
bool use_extended_id_; bool use_extended_id_;
CanSpeed bit_rate_; CanSpeed bit_rate_;
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
virtual bool setup_internal(); virtual bool setup_internal();
virtual Error send_message(struct CanFrame *frame); virtual Error send_message(struct CanFrame *frame);

View File

@ -32,14 +32,14 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(CCS811Component), cv.GenerateID(): cv.declare_id(CCS811Component),
cv.Required(CONF_ECO2): sensor.sensor_schema( cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION, unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2, icon=ICON_MOLECULE_CO2,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_TVOC): sensor.sensor_schema( cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION, unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR, icon=ICON_RADIATOR,
accuracy_decimals=0, accuracy_decimals=0,
@ -64,10 +64,13 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2]) if eco2_config := config.get(CONF_ECO2):
cg.add(var.set_co2(sens)) sens = await sensor.new_sensor(eco2_config)
sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_co2(sens))
cg.add(var.set_tvoc(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION): if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config) sens = await text_sensor.new_text_sensor(version_config)

View File

@ -11,9 +11,11 @@ from esphome.const import (
CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_FAN_MODE,
CONF_CUSTOM_PRESET, CONF_CUSTOM_PRESET,
CONF_ENTITY_CATEGORY,
CONF_FAN_MODE, CONF_FAN_MODE,
CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC,
CONF_FAN_MODE_STATE_TOPIC, CONF_FAN_MODE_STATE_TOPIC,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MAX_TEMPERATURE, CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE, CONF_MIN_TEMPERATURE,
@ -46,6 +48,7 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@ -151,12 +154,11 @@ ControlTrigger = climate_ns.class_(
"ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref")) "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
) )
CLIMATE_SCHEMA = ( _CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(Climate),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema( cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{ {
@ -245,6 +247,31 @@ CLIMATE_SCHEMA = (
) )
def climate_schema(
class_: MockObjClass,
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _CLIMATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
CLIMATE_SCHEMA = climate_schema(Climate)
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config): async def setup_climate_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
@ -419,6 +446,12 @@ async def register_climate(var, config):
await setup_climate_core_(var, config) await setup_climate_core_(var, config)
async def new_climate(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_climate(var, config)
return var
CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.use_id(Climate), cv.Required(CONF_ID): cv.use_id(Climate),

View File

@ -20,7 +20,7 @@ enum ClimateMode : uint8_t {
CLIMATE_MODE_FAN_ONLY = 4, CLIMATE_MODE_FAN_ONLY = 4,
/// The climate device is set to dry/humidity mode /// The climate device is set to dry/humidity mode
CLIMATE_MODE_DRY = 5, CLIMATE_MODE_DRY = 5,
/** The climate device is adjusting the temperatre dynamically. /** The climate device is adjusting the temperature dynamically.
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */

View File

@ -40,24 +40,24 @@ namespace climate {
*/ */
class ClimateTraits { class ClimateTraits {
public: public:
bool get_supports_current_temperature() const { return supports_current_temperature_; } bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
void set_supports_current_temperature(bool supports_current_temperature) { void set_supports_current_temperature(bool supports_current_temperature) {
supports_current_temperature_ = supports_current_temperature; this->supports_current_temperature_ = supports_current_temperature;
} }
bool get_supports_current_humidity() const { return supports_current_humidity_; } bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
void set_supports_current_humidity(bool supports_current_humidity) { void set_supports_current_humidity(bool supports_current_humidity) {
supports_current_humidity_ = supports_current_humidity; this->supports_current_humidity_ = supports_current_humidity;
} }
bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
supports_two_point_target_temperature_ = supports_two_point_target_temperature; this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
} }
bool get_supports_target_humidity() const { return supports_target_humidity_; } bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
void set_supports_target_humidity(bool supports_target_humidity) { void set_supports_target_humidity(bool supports_target_humidity) {
supports_target_humidity_ = supports_target_humidity; this->supports_target_humidity_ = supports_target_humidity;
} }
void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@ -72,15 +72,15 @@ class ClimateTraits {
} }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return supported_modes_; } const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supports_action(bool supports_action) { supports_action_ = supports_action; } void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
bool get_supports_action() const { return supports_action_; } bool get_supports_action() const { return this->supports_action_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); } void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
@ -99,35 +99,37 @@ class ClimateTraits {
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); } bool get_supports_fan_modes() const {
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return supported_fan_modes_; } return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
}
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
const std::set<std::string> &get_supported_custom_fan_modes() const { return supported_custom_fan_modes_; } const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return supported_custom_fan_modes_.count(custom_fan_mode); return this->supported_custom_fan_modes_.count(custom_fan_mode);
} }
void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); } void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; } const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
const std::set<std::string> &get_supported_custom_presets() const { return supported_custom_presets_; } const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return supported_custom_presets_.count(custom_preset); return this->supported_custom_presets_.count(custom_preset);
} }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
@ -138,54 +140,58 @@ class ClimateTraits {
void set_supports_swing_mode_horizontal(bool supported) { void set_supports_swing_mode_horizontal(bool supported) {
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
} }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return supported_swing_modes_; } const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } void set_visual_min_temperature(float visual_min_temperature) {
float get_visual_max_temperature() const { return visual_max_temperature_; } this->visual_min_temperature_ = visual_min_temperature;
void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } }
float get_visual_target_temperature_step() const { return visual_target_temperature_step_; } float get_visual_max_temperature() const { return this->visual_max_temperature_; }
float get_visual_current_temperature_step() const { return visual_current_temperature_step_; } void set_visual_max_temperature(float visual_max_temperature) {
this->visual_max_temperature_ = visual_max_temperature;
}
float get_visual_target_temperature_step() const { return this->visual_target_temperature_step_; }
float get_visual_current_temperature_step() const { return this->visual_current_temperature_step_; }
void set_visual_target_temperature_step(float temperature_step) { void set_visual_target_temperature_step(float temperature_step) {
visual_target_temperature_step_ = temperature_step; this->visual_target_temperature_step_ = temperature_step;
} }
void set_visual_current_temperature_step(float temperature_step) { void set_visual_current_temperature_step(float temperature_step) {
visual_current_temperature_step_ = temperature_step; this->visual_current_temperature_step_ = temperature_step;
} }
void set_visual_temperature_step(float temperature_step) { void set_visual_temperature_step(float temperature_step) {
visual_target_temperature_step_ = temperature_step; this->visual_target_temperature_step_ = temperature_step;
visual_current_temperature_step_ = temperature_step; this->visual_current_temperature_step_ = temperature_step;
} }
int8_t get_target_temperature_accuracy_decimals() const; int8_t get_target_temperature_accuracy_decimals() const;
int8_t get_current_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const;
float get_visual_min_humidity() const { return visual_min_humidity_; } float get_visual_min_humidity() const { return this->visual_min_humidity_; }
void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } void set_visual_min_humidity(float visual_min_humidity) { this->visual_min_humidity_ = visual_min_humidity; }
float get_visual_max_humidity() const { return visual_max_humidity_; } float get_visual_max_humidity() const { return this->visual_max_humidity_; }
void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
supported_modes_.insert(mode); this->supported_modes_.insert(mode);
} else { } else {
supported_modes_.erase(mode); this->supported_modes_.erase(mode);
} }
} }
void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) {
if (supported) { if (supported) {
supported_fan_modes_.insert(mode); this->supported_fan_modes_.insert(mode);
} else { } else {
supported_fan_modes_.erase(mode); this->supported_fan_modes_.erase(mode);
} }
} }
void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) {
if (supported) { if (supported) {
supported_swing_modes_.insert(mode); this->supported_swing_modes_.insert(mode);
} else { } else {
supported_swing_modes_.erase(mode); this->supported_swing_modes_.erase(mode);
} }
} }

View File

@ -1,7 +1,13 @@
import logging
from esphome import core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["remote_transmitter"] DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"] AUTO_LOAD = ["sensor", "remote_base"]
@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_(
remote_base.RemoteTransmittable, remote_base.RemoteTransmittable,
) )
CLIMATE_IR_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( def climate_ir_schema(
class_: MockObjClass,
) -> cv.Schema:
return (
climate.climate_schema(class_)
.extend(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
def climate_ir_with_receiver_schema(
class_: MockObjClass,
) -> cv.Schema:
return climate_ir_schema(class_).extend(
{ {
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, remote_base.RemoteReceiverBase
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), ),
} }
) )
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
{ # Remove before 2025.11.0
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( def deprecated_schema_constant(config):
remote_base.RemoteReceiverBase type: str = "unknown"
), if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
} type = str(id.type).split("::", maxsplit=1)[0]
) _LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config): async def register_climate_ir(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await remote_base.register_transmittable(var, config) await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
@ -48,3 +82,9 @@ async def register_climate_ir(var, config):
if sensor_id := config.get(CONF_SENSOR): if sensor_id := config.get(CONF_SENSOR):
sens = await cg.get_variable(sensor_id) sens = await cg.get_variable(sensor_id)
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
async def new_climate_ir(config, *args):
var = await climate.new_climate(config, *args)
await register_climate_ir(var, config)
return var

View File

@ -1,7 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low" CONF_BIT_ZERO_LOW = "bit_zero_low"
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(LgIrClimate),
cv.Optional( cv.Optional(
CONF_HEADER_HIGH, default="8000us" CONF_HEADER_HIGH, default="8000us"
): cv.positive_time_period_microseconds, ): cv.positive_time_period_microseconds,
@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)
cg.add(var.set_header_high(config[CONF_HEADER_HIGH])) cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
cg.add(var.set_header_low(config[CONF_HEADER_LOW])) cg.add(var.set_header_low(config[CONF_HEADER_LOW]))

View File

@ -32,7 +32,7 @@ const uint32_t FAN_MAX = 0x40;
// Temperature // Temperature
const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1;
const uint32_t TEMP_MASK = 0XF00; const uint32_t TEMP_MASK = 0xF00;
const uint32_t TEMP_SHIFT = 8; const uint32_t TEMP_SHIFT = 8;
const uint16_t BITS = 28; const uint16_t BITS = 28;
@ -43,11 +43,11 @@ void LgIrClimate::transmit_state() {
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
// Set command // Set command
if (send_swing_cmd_) { if (this->send_swing_cmd_) {
send_swing_cmd_ = false; this->send_swing_cmd_ = false;
remote_state |= COMMAND_SWING; remote_state |= COMMAND_SWING;
} else { } else {
bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF); bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_COOL:
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL; remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
@ -71,7 +71,7 @@ void LgIrClimate::transmit_state() {
} }
} }
mode_before_ = this->mode; this->mode_before_ = this->mode;
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
@ -102,7 +102,7 @@ void LgIrClimate::transmit_state() {
remote_state |= ((temp - 15) << TEMP_SHIFT); remote_state |= ((temp - 15) << TEMP_SHIFT);
} }
transmit_(remote_state); this->transmit_(remote_state);
this->publish_state(); this->publish_state();
} }
@ -187,7 +187,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
} }
void LgIrClimate::transmit_(uint32_t value) { void LgIrClimate::transmit_(uint32_t value) {
calc_checksum_(value); this->calc_checksum_(value);
ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value); ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value);
auto transmit = this->transmitter_->transmit(); auto transmit = this->transmitter_->transmit();

View File

@ -21,7 +21,7 @@ class LgIrClimate : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device. /// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override { void control(const climate::ClimateCall &call) override {
send_swing_cmd_ = call.get_swing_mode().has_value(); this->send_swing_cmd_ = call.get_swing_mode().has_value();
// swing resets after unit powered off // swing resets after unit powered off
if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF)
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;

View File

@ -3,6 +3,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE
ColorStruct = cg.esphome_ns.struct("Color") ColorStruct = cg.esphome_ns.struct("Color")
INSTANCE_TYPE = ColorStruct
MULTI_CONF = True MULTI_CONF = True
CONF_RED_INT = "red_int" CONF_RED_INT = "red_int"

View File

@ -0,0 +1,5 @@
"""Constants used by esphome components."""
CODEOWNERS = ["@esphome/core"]
CONF_DRAW_ROUNDING = "draw_rounding"

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet"] CODEOWNERS = ["@glmnet"]
@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"]
coolix_ns = cg.esphome_ns.namespace("coolix") coolix_ns = cg.esphome_ns.namespace("coolix")
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
{
cv.GenerateID(): cv.declare_id(CoolixClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -5,7 +5,6 @@ from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_ICON, CONF_ICON,
CONF_ID,
CONF_SOURCE_ID, CONF_SOURCE_ID,
) )
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
@ -15,12 +14,15 @@ from .. import copy_ns
CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component) CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
{ cover.cover_schema(CopyCover)
cv.GenerateID(): cv.declare_id(CopyCover), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
await cover.register_cover(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
from .. import copy_ns from .. import copy_ns
@ -9,12 +9,15 @@ from .. import copy_ns
CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( CONFIG_SCHEMA = (
{ fan.fan_schema(CopyFan)
cv.GenerateID(): cv.declare_id(CopyFan), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await fan.new_fan(config)
await fan.register_fan(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import lock from esphome.components import lock
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
from .. import copy_ns from .. import copy_ns
@ -9,12 +9,15 @@ from .. import copy_ns
CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component) CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component)
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( CONFIG_SCHEMA = (
{ lock.lock_schema(CopyLock)
cv.GenerateID(): cv.declare_id(CopyLock), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await lock.new_lock(config)
await lock.register_lock(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@ -9,12 +9,15 @@ from .. import copy_ns
CopyText = copy_ns.class_("CopyText", text.Text, cg.Component) CopyText = copy_ns.class_("CopyText", text.Text, cg.Component)
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( CONFIG_SCHEMA = (
{ text.text_schema(CopyText)
cv.GenerateID(): cv.declare_id(CopyText), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),

View File

@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_OPEN, CONF_ON_OPEN,
@ -31,6 +33,7 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@ -89,12 +92,11 @@ CoverClosedTrigger = cover_ns.class_(
CONF_ON_CLOSED = "on_closed" CONF_ON_CLOSED = "on_closed"
COVER_SCHEMA = ( _COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(Cover),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
@ -124,6 +126,33 @@ COVER_SCHEMA = (
) )
def cover_schema(
class_: MockObjClass,
*,
device_class: str = cv.UNDEFINED,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _COVER_SCHEMA.extend(schema)
# Remove before 2025.11.0
COVER_SCHEMA = cover_schema(Cover)
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config): async def setup_cover_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
@ -163,6 +192,12 @@ async def register_cover(var, config):
await setup_cover_core_(var, config) await setup_cover_core_(var, config)
async def new_cover(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_cover(var, config)
return var
COVER_ACTION_SCHEMA = maybe_simple_id( COVER_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.Required(CONF_ID): cv.use_id(Cover), cv.Required(CONF_ID): cv.use_id(Cover),

View File

@ -1,5 +1,6 @@
#include "cse7766.h" #include "cse7766.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
@ -7,7 +8,7 @@ namespace cse7766 {
static const char *const TAG = "cse7766"; static const char *const TAG = "cse7766";
void CSE7766Component::loop() { void CSE7766Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) { if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
this->raw_data_index_ = 0; this->raw_data_index_ = 0;

View File

@ -0,0 +1,28 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from .. import cst226_ns
from ..touchscreen import CST226ButtonListener, CST226Touchscreen
CONF_CST226_ID = "cst226_id"
CST226Button = cst226_ns.class_(
"CST226Button",
binary_sensor.BinarySensor,
cg.Component,
CST226ButtonListener,
cg.Parented.template(CST226Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend(
{
cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_CST226_ID])

View File

@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "../touchscreen/cst226_touchscreen.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace cst226 {
class CST226Button : public binary_sensor::BinarySensor,
public Component,
public CST226ButtonListener,
public Parented<CST226Touchscreen> {
public:
void setup() override;
void dump_config() override;
void update_button(bool state) override;
};
} // namespace cst226
} // namespace esphome

View File

@ -0,0 +1,19 @@
#include "cs226_button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace cst226 {
static const char *const TAG = "CST226.binary_sensor";
void CST226Button::setup() {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void CST226Button::dump_config() { LOG_BINARY_SENSOR("", "CST226 Button", this); }
void CST226Button::update_button(bool state) { this->publish_state(state); }
} // namespace cst226
} // namespace esphome

View File

@ -3,8 +3,10 @@
namespace esphome { namespace esphome {
namespace cst226 { namespace cst226 {
static const char *const TAG = "cst226.touchscreen";
void CST226Touchscreen::setup() { void CST226Touchscreen::setup() {
esph_log_config(TAG, "Setting up CST226 Touchscreen..."); ESP_LOGCONFIG(TAG, "Setting up CST226 Touchscreen...");
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup(); this->reset_pin_->setup();
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
@ -26,6 +28,11 @@ void CST226Touchscreen::update_touches() {
return; return;
} }
this->status_clear_warning(); this->status_clear_warning();
if (data[0] == 0x83 && data[1] == 0x17 && data[5] == 0x80) {
this->update_button_state_(true);
return;
}
this->update_button_state_(false);
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) { if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
this->skip_update_ = true; this->skip_update_ = true;
return; return;
@ -43,13 +50,21 @@ void CST226Touchscreen::update_touches() {
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F); int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
int16_t z = data[index + 4]; int16_t z = data[index + 4];
this->add_raw_touch_position_(id, x, y, z); this->add_raw_touch_position_(id, x, y, z);
esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y); ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
index += 5; index += 5;
if (i == 0) if (i == 0)
index += 2; index += 2;
} }
} }
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
this->mark_failed();
return false;
}
return true;
}
void CST226Touchscreen::continue_setup_() { void CST226Touchscreen::continue_setup_() {
uint8_t buffer[8]; uint8_t buffer[8];
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
@ -58,7 +73,7 @@ void CST226Touchscreen::continue_setup_() {
} }
buffer[0] = 0xD1; buffer[0] = 0xD1;
if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) { if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) {
esph_log_e(TAG, "Write byte to 0xD1 failed"); ESP_LOGE(TAG, "Write byte to 0xD1 failed");
this->mark_failed(); this->mark_failed();
return; return;
} }
@ -66,7 +81,7 @@ void CST226Touchscreen::continue_setup_() {
if (this->read16_(0xD204, buffer, 4)) { if (this->read16_(0xD204, buffer, 4)) {
uint16_t chip_id = buffer[2] + (buffer[3] << 8); uint16_t chip_id = buffer[2] + (buffer[3] << 8);
uint16_t project_id = buffer[0] + (buffer[1] << 8); uint16_t project_id = buffer[0] + (buffer[1] << 8);
esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id); ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
} }
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
if (this->read16_(0xD1F8, buffer, 4)) { if (this->read16_(0xD1F8, buffer, 4)) {
@ -80,7 +95,14 @@ void CST226Touchscreen::continue_setup_() {
} }
} }
this->setup_complete_ = true; this->setup_complete_ = true;
esph_log_config(TAG, "CST226 Touchscreen setup complete"); ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete");
}
void CST226Touchscreen::update_button_state_(bool state) {
if (this->button_touched_ == state)
return;
this->button_touched_ = state;
for (auto *listener : this->button_listeners_)
listener->update_button(state);
} }
void CST226Touchscreen::dump_config() { void CST226Touchscreen::dump_config() {

View File

@ -9,10 +9,13 @@
namespace esphome { namespace esphome {
namespace cst226 { namespace cst226 {
static const char *const TAG = "cst226.touchscreen";
static const uint8_t CST226_REG_STATUS = 0x00; static const uint8_t CST226_REG_STATUS = 0x00;
class CST226ButtonListener {
public:
virtual void update_button(bool state) = 0;
};
class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
@ -22,22 +25,19 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); } bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
protected: protected:
bool read16_(uint16_t addr, uint8_t *data, size_t len) { bool read16_(uint16_t addr, uint8_t *data, size_t len);
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
esph_log_e(TAG, "Read data from 0x%04X failed", addr);
this->mark_failed();
return false;
}
return true;
}
void continue_setup_(); void continue_setup_();
void update_button_state_(bool state);
InternalGPIOPin *interrupt_pin_{}; InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{}; GPIOPin *reset_pin_{};
uint8_t chip_id_{}; uint8_t chip_id_{};
bool setup_complete_{}; bool setup_complete_{};
std::vector<CST226ButtonListener *> button_listeners_;
bool button_touched_{};
}; };
} // namespace cst226 } // namespace cst226

View File

@ -5,7 +5,6 @@ import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION, CONF_CLOSE_DURATION,
CONF_ID,
CONF_MAX_DURATION, CONF_MAX_DURATION,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
CONF_OPEN_DURATION, CONF_OPEN_DURATION,
@ -30,45 +29,47 @@ CurrentBasedCover = current_based_ns.class_(
"CurrentBasedCover", cover.Cover, cg.Component "CurrentBasedCover", cover.Cover, cg.Component
) )
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
{ cover.cover_schema(CurrentBasedCover)
cv.GenerateID(): cv.declare_id(CurrentBasedCover), .extend(
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), {
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
min=0, min_included=False cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( ),
min=0, min_included=False cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), ),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range( cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
min=0, min_included=False cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( ),
min=0, min_included=False cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), ),
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean, cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation( cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
single=True cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
), single=True
cv.Optional( ),
CONF_START_SENSING_DELAY, default="500ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_START_SENSING_DELAY, default="500ms"
} ): cv.positive_time_period_milliseconds,
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await cover.register_cover(var, config)
await automation.build_automation( await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION] var.get_stop_trigger(), [], config[CONF_STOP_ACTION]

View File

@ -1,6 +1,7 @@
#include "current_based_cover.h" #include "current_based_cover.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cfloat> #include <cfloat>
namespace esphome { namespace esphome {
@ -60,7 +61,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING) { if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction

View File

@ -1,20 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
daikin_ns = cg.esphome_ns.namespace("daikin") daikin_ns = cg.esphome_ns.namespace("daikin")
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
{
cv.GenerateID(): cv.declare_id(DaikinClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -65,7 +65,7 @@ void DaikinClimate::transmit_state() {
transmit.perform(); transmit.perform();
} }
uint8_t DaikinClimate::operation_mode_() { uint8_t DaikinClimate::operation_mode_() const {
uint8_t operating_mode = DAIKIN_MODE_ON; uint8_t operating_mode = DAIKIN_MODE_ON;
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_COOL:
@ -92,9 +92,12 @@ uint8_t DaikinClimate::operation_mode_() {
return operating_mode; return operating_mode;
} }
uint16_t DaikinClimate::fan_speed_() { uint16_t DaikinClimate::fan_speed_() const {
uint16_t fan_speed; uint16_t fan_speed;
switch (this->fan_mode.value()) { switch (this->fan_mode.value()) {
case climate::CLIMATE_FAN_QUIET:
fan_speed = DAIKIN_FAN_SILENT << 8;
break;
case climate::CLIMATE_FAN_LOW: case climate::CLIMATE_FAN_LOW:
fan_speed = DAIKIN_FAN_1 << 8; fan_speed = DAIKIN_FAN_1 << 8;
break; break;
@ -126,12 +129,11 @@ uint16_t DaikinClimate::fan_speed_() {
return fan_speed; return fan_speed;
} }
uint8_t DaikinClimate::temperature_() { uint8_t DaikinClimate::temperature_() const {
// Force special temperatures depending on the mode // Force special temperatures depending on the mode
switch (this->mode) { switch (this->mode) {
case climate::CLIMATE_MODE_FAN_ONLY: case climate::CLIMATE_MODE_FAN_ONLY:
return 0x32; return 0x32;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_DRY: case climate::CLIMATE_MODE_DRY:
return 0xc0; return 0xc0;
default: default:
@ -148,19 +150,25 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum)
return false; return false;
uint8_t mode = frame[5]; uint8_t mode = frame[5];
// Temperature is given in degrees celcius * 2
// only update for states that use the temperature
uint8_t temperature = frame[6];
if (mode & DAIKIN_MODE_ON) { if (mode & DAIKIN_MODE_ON) {
switch (mode & 0xF0) { switch (mode & 0xF0) {
case DAIKIN_MODE_COOL: case DAIKIN_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL; this->mode = climate::CLIMATE_MODE_COOL;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_DRY: case DAIKIN_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY; this->mode = climate::CLIMATE_MODE_DRY;
break; break;
case DAIKIN_MODE_HEAT: case DAIKIN_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT; this->mode = climate::CLIMATE_MODE_HEAT;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_AUTO: case DAIKIN_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->target_temperature = static_cast<float>(temperature * 0.5f);
break; break;
case DAIKIN_MODE_FAN: case DAIKIN_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY; this->mode = climate::CLIMATE_MODE_FAN_ONLY;
@ -169,10 +177,6 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
} else { } else {
this->mode = climate::CLIMATE_MODE_OFF; this->mode = climate::CLIMATE_MODE_OFF;
} }
uint8_t temperature = frame[6];
if (!(temperature & 0xC0)) {
this->target_temperature = temperature >> 1;
}
uint8_t fan_mode = frame[8]; uint8_t fan_mode = frame[8];
uint8_t swing_mode = frame[9]; uint8_t swing_mode = frame[9];
if (fan_mode & 0xF && swing_mode & 0xF) { if (fan_mode & 0xF && swing_mode & 0xF) {
@ -187,7 +191,6 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
switch (fan_mode & 0xF0) { switch (fan_mode & 0xF0) {
case DAIKIN_FAN_1: case DAIKIN_FAN_1:
case DAIKIN_FAN_2: case DAIKIN_FAN_2:
case DAIKIN_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_LOW; this->fan_mode = climate::CLIMATE_FAN_LOW;
break; break;
case DAIKIN_FAN_3: case DAIKIN_FAN_3:
@ -200,6 +203,9 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) {
case DAIKIN_FAN_AUTO: case DAIKIN_FAN_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO; this->fan_mode = climate::CLIMATE_FAN_AUTO;
break; break;
case DAIKIN_FAN_SILENT:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
} }
this->publish_state(); this->publish_state();
return true; return true;

View File

@ -44,17 +44,17 @@ class DaikinClimate : public climate_ir::ClimateIR {
public: public:
DaikinClimate() DaikinClimate()
: climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, : climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, {climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_HIGH}, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
protected: protected:
// Transmit via IR the state of this climate controller. // Transmit via IR the state of this climate controller.
void transmit_state() override; void transmit_state() override;
uint8_t operation_mode_(); uint8_t operation_mode_() const;
uint16_t fan_speed_(); uint16_t fan_speed_() const;
uint8_t temperature_(); uint8_t temperature_() const;
// Handle received IR Buffer // Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override; bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]); bool parse_state_frame_(const uint8_t frame[]);

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