Merge pull request #8783 from esphome/bump-2025.5.0b1

2025.5.0b1
This commit is contained in:
Jesse Hills 2025-05-14 15:54:44 +12:00 committed by GitHub
commit 1e20440c8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
505 changed files with 29328 additions and 5250 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.16.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.16.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

@ -53,7 +53,7 @@ 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 - name: Set up python environment
@ -68,31 +68,31 @@ jobs:
uses: pypa/gh-action-pypi-publish@v1.12.4 uses: pypa/gh-action-pypi-publish@v1.12.4
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 +109,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 +151,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 +164,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 +200,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 +231,25 @@ 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
- deploy-manifest
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

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

View File

@ -1,124 +1,47 @@
# 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 RUN pip install uv==0.6.14
COPY requirements.txt /
ARG TARGETARCH
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
@ -139,43 +62,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 +76,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

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

@ -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):
decoded = base64.b64decode(key)
cg.add(var.set_noise_psk(list(decoded))) 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;

View File

@ -62,7 +62,14 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
: parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) { : parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) {
this->proto_write_buffer_.reserve(64); this->proto_write_buffer_.reserve(64);
#if defined(USE_API_PLAINTEXT) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
} else {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
}
#elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
@ -185,15 +192,34 @@ void APIConnection::loop() {
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); // Message will use 8 more bytes than the minimum size, and typical
auto buffer = this->create_buffer(); // MTU is 1500. Sometimes users will see as low as 1460 MTU.
// If its IPv6 the header is 40 bytes, and if its IPv4
// the header is 20 bytes. So we have 1460 - 40 = 1420 bytes
// available for the payload. But we also need to add the size of
// the protobuf overhead, which is 8 bytes.
//
// To be safe we pick 1390 bytes as the maximum size
// to send in one go. This is the maximum size of a single packet
// that can be sent over the network.
// This is to avoid fragmentation of the packet.
uint32_t to_send = std::min((size_t) 1390, this->image_reader_.available());
bool done = this->image_reader_.available() == to_send;
uint32_t msg_size = 0;
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
// partial message size calculated manually since its a special case
// 1 for the data field, varint for the data size, and the data itself
msg_size += 1 + ProtoSize::varint(to_send) + to_send;
ProtoSize::add_bool_field(msg_size, 1, done);
auto buffer = this->create_buffer(msg_size);
// fixed32 key = 1; // fixed32 key = 1;
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
// bytes data = 2; // bytes data = 2;
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
// bool done = 3; // bool done = 3;
bool done = this->image_reader_.available() == to_send;
buffer.encode_bool(3, done); buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44); bool success = this->send_buffer(buffer, 44);
if (success) { if (success) {
@ -1468,6 +1494,11 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
return resp; return resp;
} }
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
}
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@ -1762,12 +1793,25 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
if (this->log_subscription_ < level) if (this->log_subscription_ < level)
return false; return false;
// Send raw so that we don't copy too much // Pre-calculate message size to avoid reallocations
auto buffer = this->create_buffer(); const size_t line_length = strlen(line);
// LogLevel level = 1; uint32_t msg_size = 0;
buffer.encode_uint32(1, static_cast<uint32_t>(level));
// string message = 3; // Add size for level field (field ID 1, varint type)
buffer.encode_string(3, line, strlen(line)); // 1 byte for field tag + size of the level varint
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(level));
// Add size for string field (field ID 3, string type)
// 1 byte for field tag + size of length varint + string length
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(line_length)) + line_length;
// Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size);
// Encode the message (SubscribeLogsResponse)
buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1
buffer.encode_string(3, line, line_length); // string message = 3
// SubscribeLogsResponse - 29 // SubscribeLogsResponse - 29
return this->send_buffer(buffer, 29); return this->send_buffer(buffer, 29);
} }
@ -1848,6 +1892,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#endif #endif
return resp; return resp;
} }
@ -1869,6 +1916,26 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
ESP_LOGV(TAG, "Could not find matching service!"); ESP_LOGV(TAG, "Could not find matching service!");
} }
} }
#ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
psk_t psk{};
NoiseEncryptionSetKeyResponse resp;
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
resp.success = false;
return resp;
}
if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
resp.success = false;
return resp;
}
resp.success = true;
return resp;
}
#endif
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
state_subs_at_ = 0; state_subs_at_ = 0;
} }

View File

@ -221,6 +221,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
@ -300,6 +301,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,9 +312,10 @@ 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();
this->proto_write_buffer_.reserve(reserve_size);
return {&this->proto_write_buffer_}; return {&this->proto_write_buffer_};
} }
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;

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
@ -546,71 +632,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
@ -744,6 +765,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
@ -933,6 +959,8 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay
} }
std::vector<uint8_t> header; std::vector<uint8_t> header;
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) +
api::ProtoSize::varint(static_cast<uint32_t>(type)));
header.push_back(0x00); header.push_back(0x00);
ProtoVarInt(payload_len).encode(header); ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header); ProtoVarInt(type).encode(header);
@ -966,71 +994,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 +1011,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

@ -72,6 +72,12 @@ class APIFrameHelper {
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;
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);
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@ -103,7 +109,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);
@ -164,7 +172,9 @@ 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_;

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_) {
// 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->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

@ -149,6 +149,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 +169,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 +183,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 +212,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 +266,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 +288,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 +311,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

@ -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,117 +11,86 @@ 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_());
} }
} }
}
void ATM90E32Component::update() { void ATM90E32Component::update() {
if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) { if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) {
@ -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,33 +133,83 @@ 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->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 this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
} }
@ -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;
const uint32_t voltage_offset = ~shifted_value + 1;
return voltage_offset & 0xFFFF; // Take the lower 16 bits
} }
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { float ref_voltages[3] = {
this->get_reference_voltage(0),
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_();
}
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 = {}
if bits_per_sample is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE), max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
)(bits_per_sample) )(bits_per_sample)
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_BITS_PER_SAMPLE} {str(exc)}" error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {bits_per_sample} bits per sample."
) from exc else:
error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
raise cv.Invalid(error_string) from exc
if channels is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_CHANNELS), min=audio_config.get(CONF_MIN_CHANNELS),
max=audio_config.get(CONF_MAX_CHANNELS), max=audio_config.get(CONF_MAX_CHANNELS),
)(channels) )(channels)
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_NUM_CHANNELS} {str(exc)}" error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {channels} channels."
) from exc else:
error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
raise cv.Invalid(error_string) from exc
if sample_rate is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_SAMPLE_RATE), min=audio_config.get(CONF_MIN_SAMPLE_RATE),
max=audio_config.get(CONF_MAX_SAMPLE_RATE), max=audio_config.get(CONF_MAX_SAMPLE_RATE),
)(sample_rate) )(sample_rate)
return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
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

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

@ -15,21 +15,17 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state)) if (!this->publish_dedup_.next(state))
return; return;
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false); this->send_state_internal(state);
} else { } else {
this->filter_list_->input(state, false); this->filter_list_->input(state);
} }
} }
void BinarySensor::publish_initial_state(bool state) { void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state)) this->has_state_ = false;
return; this->publish_state(state);
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
} }
} void BinarySensor::send_state_internal(bool state) {
void BinarySensor::send_state_internal(bool state, bool is_initial) { bool is_initial = !this->has_state_;
if (is_initial) { if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else { } else {

View File

@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial); void send_state_internal(bool state);
/// Return whether this binary sensor has outputted a state. /// Return whether this binary sensor has outputted a state.
virtual bool has_state() const; virtual bool has_state() const;

View File

@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) { void Filter::output(bool value) {
if (!this->dedup_.next(value)) if (!this->dedup_.next(value))
return; return;
if (this->next_ == nullptr) { if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial); this->parent_->send_state_internal(value);
} else { } else {
this->next_->input(value, is_initial); this->next_->input(value);
} }
} }
void Filter::input(bool value, bool is_initial) { void Filter::input(bool value) {
auto b = this->new_value(value, is_initial); auto b = this->new_value(value);
if (b.has_value()) { if (b.has_value()) {
this->output(*b, is_initial); this->output(*b);
} }
} }
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else { } else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
} }
return {}; return {};
} }
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout("ON");
@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) { optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout("OFF");
@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; } optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) { optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) { if (value) {
// Ignore if already running // Ignore if already running
if (this->active_timing_ != 0) if (this->active_timing_ != 0)
@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) { void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val, false); // This is at least the second one so not initial this->output(val);
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
} }
@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) { optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) { if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true; this->steady_ = true;
this->output(value, is_initial); this->output(value);
}); });
return {}; return {};
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value, is_initial); this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }

View File

@ -14,11 +14,11 @@ class BinarySensor;
class Filter { class Filter {
public: public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0; virtual optional<bool> new_value(bool value) = 0;
void input(bool value, bool is_initial); void input(bool value);
void output(bool value, bool is_initial); void output(bool value);
protected: protected:
friend BinarySensor; friend BinarySensor;
@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component { class DelayedOnOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter { class InvertFilter : public Filter {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
}; };
struct AutorepeatFilterTiming { struct AutorepeatFilterTiming {
@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public: public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public: public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f); explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
protected: protected:
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value, bool is_initial) override; optional<bool> new_value(bool value) override;
float get_setup_priority() const override; float get_setup_priority() const override;

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

@ -25,6 +25,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;
@ -40,6 +56,9 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
return false; return false;
api::BluetoothLERawAdvertisementsResponse resp; api::BluetoothLERawAdvertisementsResponse resp;
// Pre-allocate the advertisements vector to avoid reallocations
resp.advertisements.reserve(count);
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; api::BluetoothLERawAdvertisement adv;
@ -49,9 +68,8 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
uint8_t length = result.adv_data_len + result.scan_rsp_len; uint8_t length = result.adv_data_len + result.scan_rsp_len;
adv.data.reserve(length); adv.data.reserve(length);
for (uint16_t i = 0; i < length; i++) { // Use a bulk insert instead of individual push_backs
adv.data.push_back(result.ble_adv[i]); adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]);
}
resp.advertisements.push_back(std::move(adv)); resp.advertisements.push_back(std::move(adv));
@ -69,21 +87,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()) {
// 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.push_back(uuid.to_string()); resp.service_uuids.push_back(uuid.to_string());
} }
for (auto &data : device.get_service_datas()) {
// Pre-allocate service data vector
auto service_datas = device.get_service_datas();
resp.service_data.reserve(service_datas.size());
for (auto &data : service_datas) {
api::BluetoothServiceData service_data; api::BluetoothServiceData service_data;
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)); resp.service_data.push_back(std::move(service_data));
} }
for (auto &data : device.get_manufacturer_datas()) {
// Pre-allocate manufacturer data vector
auto manufacturer_datas = device.get_manufacturer_datas();
resp.manufacturer_data.reserve(manufacturer_datas.size());
for (auto &data : manufacturer_datas) {
api::BluetoothServiceData manufacturer_data; api::BluetoothServiceData manufacturer_data;
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)); 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);
} }
@ -145,11 +176,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 +218,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 +517,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 +591,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,6 +54,7 @@ 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;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
@ -84,6 +86,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 +111,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 +129,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

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

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

@ -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)
.extend(
{ {
cv.GenerateID(): cv.declare_id(CopyCover),
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 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)
.extend(
{ {
cv.GenerateID(): cv.declare_id(CopyLock),
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)
.extend(
{ {
cv.GenerateID(): cv.declare_id(CopyText),
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

@ -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,9 +29,10 @@ 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)
.extend(
{ {
cv.GenerateID(): cv.declare_id(CurrentBasedCover),
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
@ -62,13 +62,14 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
CONF_START_SENSING_DELAY, default="500ms" CONF_START_SENSING_DELAY, default="500ms"
): cv.positive_time_period_milliseconds, ): 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

@ -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[]);

View File

@ -56,21 +56,13 @@ void DallasTemperatureSensor::update() {
}); });
} }
void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() { bool DallasTemperatureSensor::read_scratch_pad_() {
bool success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
if (success) {
for (uint8_t &i : this->scratch_pad_) { for (uint8_t &i : this->scratch_pad_) {
i = this->bus_->read8(); i = this->bus_->read8();
} }
} } else {
bool DallasTemperatureSensor::read_scratch_pad_() {
bool success;
{
InterruptLock lock;
success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
if (success)
this->read_scratch_pad_int_();
}
if (!success) {
ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str());
this->status_set_warning("bus reset failed"); this->status_set_warning("bus reset failed");
} }
@ -113,8 +105,6 @@ void DallasTemperatureSensor::setup() {
return; return;
this->scratch_pad_[4] = res; this->scratch_pad_[4] = res;
{
InterruptLock lock;
if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) { if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) {
this->bus_->write8(this->scratch_pad_[2]); // high alarm temp this->bus_->write8(this->scratch_pad_[2]); // high alarm temp
this->bus_->write8(this->scratch_pad_[3]); // low alarm temp this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
@ -124,7 +114,6 @@ void DallasTemperatureSensor::setup() {
// write value to EEPROM // write value to EEPROM
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD); this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
} }
}
bool DallasTemperatureSensor::check_scratch_pad_() { bool DallasTemperatureSensor::check_scratch_pad_() {
bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
@ -138,6 +127,10 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
if (!chksum_validity) { if (!chksum_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
this->status_set_warning("scratch pad checksum invalid"); this->status_set_warning("scratch pad checksum invalid");
ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
} }
return chksum_validity; return chksum_validity;
} }

View File

@ -23,7 +23,6 @@ class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor,
/// Get the number of milliseconds we have to wait for the conversion phase. /// Get the number of milliseconds we have to wait for the conversion phase.
uint16_t millis_to_wait_for_conversion_() const; uint16_t millis_to_wait_for_conversion_() const;
bool read_scratch_pad_(); bool read_scratch_pad_();
void read_scratch_pad_int_();
bool check_scratch_pad_(); bool check_scratch_pad_();
float get_temp_c_(); float get_temp_c_();
}; };

View File

@ -1,6 +1,7 @@
#include "debug_component.h" #include "debug_component.h"
#include <algorithm> #include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@ -25,6 +26,7 @@ void DebugComponent::dump_config() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
@ -86,6 +88,9 @@ void DebugComponent::update() {
this->loop_time_sensor_->publish_state(this->max_loop_time_); this->loop_time_sensor_->publish_state(this->max_loop_time_);
this->max_loop_time_ = 0; this->max_loop_time_ = 0;
} }
if (this->cpu_frequency_sensor_ != nullptr) {
this->cpu_frequency_sensor_->publish_state(arch_get_cpu_freq_hz());
}
#endif // USE_SENSOR #endif // USE_SENSOR
update_platform_(); update_platform_();

View File

@ -34,8 +34,12 @@ class DebugComponent : public PollingComponent {
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; } void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32 #endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor;
}
#endif // USE_SENSOR #endif // USE_SENSOR
protected: protected:
uint32_t free_heap_{}; uint32_t free_heap_{};
@ -53,6 +57,7 @@ class DebugComponent : public PollingComponent {
#ifdef USE_ESP32 #ifdef USE_ESP32
sensor::Sensor *psram_sensor_{nullptr}; sensor::Sensor *psram_sensor_{nullptr};
#endif // USE_ESP32 #endif // USE_ESP32
sensor::Sensor *cpu_frequency_sensor_{nullptr};
#endif // USE_SENSOR #endif // USE_SENSOR
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -75,6 +80,7 @@ class DebugComponent : public PollingComponent {
#endif // USE_TEXT_SENSOR #endif // USE_TEXT_SENSOR
std::string get_reset_reason_(); std::string get_reset_reason_();
std::string get_wakeup_cause_();
uint32_t get_free_heap_(); uint32_t get_free_heap_();
void get_device_info_(std::string &device_info); void get_device_info_(std::string &device_info);
void update_platform_(); void update_platform_();

View File

@ -1,25 +1,18 @@
#include "debug_component.h" #include "debug_component.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <esp_sleep.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_system.h> #include <esp_system.h>
#include <esp_chip_info.h> #include <esp_chip_info.h>
#include <esp_partition.h> #include <esp_partition.h>
#if defined(USE_ESP32_VARIANT_ESP32) #include <map>
#include <esp32/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C3)
#include <esp32c3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32C6)
#include <esp32c6/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S2)
#include <esp32s2/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32S3)
#include <esp32s3/rom/rtc.h>
#elif defined(USE_ESP32_VARIANT_ESP32H2)
#include <esp32h2/rom/rtc.h>
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <Esp.h> #include <Esp.h>
#endif #endif
@ -29,6 +22,90 @@ namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
// index by values returned by esp_reset_reason
static const char *const RESET_REASONS[] = {
"unknown source",
"power-on event",
"external pin",
"software via esp_restart",
"exception/panic",
"interrupt watchdog",
"task watchdog",
"other watchdogs",
"exiting deep sleep mode",
"brownout",
"SDIO",
"USB peripheral",
"JTAG",
"efuse error",
"power glitch detected",
"CPU lock up",
};
static const char *const REBOOT_KEY = "reboot_source";
static const size_t REBOOT_MAX_LEN = 24;
// on shutdown, store the source of the reboot request
void DebugComponent::on_shutdown() {
auto *component = App.get_current_component();
char buffer[REBOOT_MAX_LEN]{};
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
if (component != nullptr) {
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
}
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
pref.save(&buffer);
global_preferences->sync();
}
std::string DebugComponent::get_reset_reason_() {
std::string reset_reason;
unsigned reason = esp_reset_reason();
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
reset_reason = RESET_REASONS[reason];
if (reason == ESP_RST_SW) {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
char buffer[REBOOT_MAX_LEN]{};
if (pref.load(&buffer)) {
reset_reason = "Reboot request from " + std::string(buffer);
}
}
} else {
reset_reason = "unknown source";
}
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
static const char *const WAKEUP_CAUSES[] = {
"undefined",
"undefined",
"external signal using RTC_IO",
"external signal using RTC_CNTL",
"timer",
"touchpad",
"ULP program",
"GPIO",
"UART",
"WIFI",
"COCPU int",
"COCPU crash",
"BT",
};
std::string DebugComponent::get_wakeup_cause_() {
const char *wake_reason;
unsigned reason = esp_sleep_get_wakeup_cause();
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
wake_reason = WAKEUP_CAUSES[reason];
} else {
wake_reason = "unknown source";
}
ESP_LOGD(TAG, "Wakeup Reason: %s", wake_reason);
return wake_reason;
}
void DebugComponent::log_partition_info_() { void DebugComponent::log_partition_info_() {
ESP_LOGCONFIG(TAG, "Partition table:"); ESP_LOGCONFIG(TAG, "Partition table:");
ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size");
@ -42,171 +119,16 @@ void DebugComponent::log_partition_info_() {
esp_partition_iterator_release(it); esp_partition_iterator_release(it);
} }
std::string DebugComponent::get_reset_reason_() {
std::string reset_reason;
switch (esp_reset_reason()) {
case ESP_RST_POWERON:
reset_reason = "Reset due to power-on event";
break;
case ESP_RST_EXT:
reset_reason = "Reset by external pin";
break;
case ESP_RST_SW:
reset_reason = "Software reset via esp_restart";
break;
case ESP_RST_PANIC:
reset_reason = "Software reset due to exception/panic";
break;
case ESP_RST_INT_WDT:
reset_reason = "Reset (software or hardware) due to interrupt watchdog";
break;
case ESP_RST_TASK_WDT:
reset_reason = "Reset due to task watchdog";
break;
case ESP_RST_WDT:
reset_reason = "Reset due to other watchdogs";
break;
case ESP_RST_DEEPSLEEP:
reset_reason = "Reset after exiting deep sleep mode";
break;
case ESP_RST_BROWNOUT:
reset_reason = "Brownout reset (software or hardware)";
break;
case ESP_RST_SDIO:
reset_reason = "Reset over SDIO";
break;
#ifdef USE_ESP32_VARIANT_ESP32
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4))
case ESP_RST_USB:
reset_reason = "Reset by USB peripheral";
break;
case ESP_RST_JTAG:
reset_reason = "Reset by JTAG";
break;
case ESP_RST_EFUSE:
reset_reason = "Reset due to efuse error";
break;
case ESP_RST_PWR_GLITCH:
reset_reason = "Reset due to power glitch detected";
break;
case ESP_RST_CPU_LOCKUP:
reset_reason = "Reset due to CPU lock up (double exception)";
break;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)
#endif // USE_ESP32_VARIANT_ESP32
default: // Includes ESP_RST_UNKNOWN
switch (rtc_get_reset_reason(0)) {
case POWERON_RESET:
reset_reason = "Power On Reset";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case SW_RESET:
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_SYS_RESET:
#endif
reset_reason = "Software Reset Digital Core";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case OWDT_RESET:
reset_reason = "Watch Dog Reset Digital Core";
break;
#endif
case DEEPSLEEP_RESET:
reset_reason = "Deep Sleep Reset Digital Core";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case SDIO_RESET:
reset_reason = "SLC Module Reset Digital Core";
break;
#endif
case TG0WDT_SYS_RESET:
reset_reason = "Timer Group 0 Watch Dog Reset Digital Core";
break;
case TG1WDT_SYS_RESET:
reset_reason = "Timer Group 1 Watch Dog Reset Digital Core";
break;
case RTCWDT_SYS_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core";
break;
#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
case INTRUSION_RESET:
reset_reason = "Intrusion Reset CPU";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
case TGWDT_CPU_RESET:
reset_reason = "Timer Group Reset CPU";
break;
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case TG0WDT_CPU_RESET:
reset_reason = "Timer Group 0 Reset CPU";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32)
case SW_CPU_RESET:
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_CPU_RESET:
#endif
reset_reason = "Software Reset CPU";
break;
case RTCWDT_CPU_RESET:
reset_reason = "RTC Watch Dog Reset CPU";
break;
#if defined(USE_ESP32_VARIANT_ESP32)
case EXT_CPU_RESET:
reset_reason = "External CPU Reset";
break;
#endif
case RTCWDT_BROWN_OUT_RESET:
reset_reason = "Voltage Unstable Reset";
break;
case RTCWDT_RTC_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module";
break;
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32C6)
case TG1WDT_CPU_RESET:
reset_reason = "Timer Group 1 Reset CPU";
break;
case SUPER_WDT_RESET:
reset_reason = "Super Watchdog Reset Digital Core And RTC Module";
break;
case EFUSE_RESET:
reset_reason = "eFuse Reset Digital Core";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
case GLITCH_RTC_RESET:
reset_reason = "Glitch Reset Digital Core And RTC Module";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case USB_UART_CHIP_RESET:
reset_reason = "USB UART Reset Digital Core";
break;
case USB_JTAG_CHIP_RESET:
reset_reason = "USB JTAG Reset Digital Core";
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
case POWER_GLITCH_RESET:
reset_reason = "Power Glitch Reset Digital Core And RTC Module";
break;
#endif
default:
reset_reason = "Unknown Reset Reason";
}
break;
}
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); } uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
static const std::map<int, const char *> CHIP_FEATURES = {
{CHIP_FEATURE_BLE, "BLE"},
{CHIP_FEATURE_BT, "BT"},
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
{CHIP_FEATURE_EMB_PSRAM, "EMB PSRAM"},
{CHIP_FEATURE_WIFI_BGN, "2.4GHz WiFi"},
};
void DebugComponent::get_device_info_(std::string &device_info) { void DebugComponent::get_device_info_(std::string &device_info) {
#if defined(USE_ARDUINO) #if defined(USE_ARDUINO)
const char *flash_mode; const char *flash_mode;
@ -242,44 +164,16 @@ void DebugComponent::get_device_info_(std::string &device_info) {
esp_chip_info_t info; esp_chip_info_t info;
esp_chip_info(&info); esp_chip_info(&info);
const char *model; const char *model = ESPHOME_VARIANT;
#if defined(USE_ESP32_VARIANT_ESP32)
model = "ESP32";
#elif defined(USE_ESP32_VARIANT_ESP32C3)
model = "ESP32-C3";
#elif defined(USE_ESP32_VARIANT_ESP32C6)
model = "ESP32-C6";
#elif defined(USE_ESP32_VARIANT_ESP32S2)
model = "ESP32-S2";
#elif defined(USE_ESP32_VARIANT_ESP32S3)
model = "ESP32-S3";
#elif defined(USE_ESP32_VARIANT_ESP32H2)
model = "ESP32-H2";
#else
model = "UNKNOWN";
#endif
std::string features; std::string features;
if (info.features & CHIP_FEATURE_EMB_FLASH) { for (auto feature : CHIP_FEATURES) {
features += "EMB_FLASH,"; if (info.features & feature.first) {
info.features &= ~CHIP_FEATURE_EMB_FLASH; features += feature.second;
features += ", ";
info.features &= ~feature.first;
} }
if (info.features & CHIP_FEATURE_WIFI_BGN) {
features += "WIFI_BGN,";
info.features &= ~CHIP_FEATURE_WIFI_BGN;
} }
if (info.features & CHIP_FEATURE_BLE) { if (info.features != 0)
features += "BLE,";
info.features &= ~CHIP_FEATURE_BLE;
}
if (info.features & CHIP_FEATURE_BT) {
features += "BT,";
info.features &= ~CHIP_FEATURE_BT;
}
if (info.features & CHIP_FEATURE_EMB_PSRAM) {
features += "EMB_PSRAM,";
info.features &= ~CHIP_FEATURE_EMB_PSRAM;
}
if (info.features)
features += "Other:" + format_hex(info.features); features += "Other:" + format_hex(info.features);
ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores, ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores,
info.revision); info.revision);
@ -289,6 +183,8 @@ void DebugComponent::get_device_info_(std::string &device_info) {
device_info += features; device_info += features;
device_info += " Cores:" + to_string(info.cores); device_info += " Cores:" + to_string(info.cores);
device_info += " Revision:" + to_string(info.revision); device_info += " Revision:" + to_string(info.revision);
device_info += str_sprintf("|CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
// Framework detection // Framework detection
device_info += "|Framework: "; device_info += "|Framework: ";
@ -315,48 +211,7 @@ void DebugComponent::get_device_info_(std::string &device_info) {
device_info += "|Reset: "; device_info += "|Reset: ";
device_info += get_reset_reason_(); device_info += get_reset_reason_();
const char *wakeup_reason; std::string wakeup_reason = this->get_wakeup_cause_();
switch (rtc_get_wakeup_cause()) {
case NO_SLEEP:
wakeup_reason = "No Sleep";
break;
case EXT_EVENT0_TRIG:
wakeup_reason = "External Event 0";
break;
case EXT_EVENT1_TRIG:
wakeup_reason = "External Event 1";
break;
case GPIO_TRIG:
wakeup_reason = "GPIO";
break;
case TIMER_EXPIRE:
wakeup_reason = "Wakeup Timer";
break;
case SDIO_TRIG:
wakeup_reason = "SDIO";
break;
case MAC_TRIG:
wakeup_reason = "MAC";
break;
case UART0_TRIG:
wakeup_reason = "UART0";
break;
case UART1_TRIG:
wakeup_reason = "UART1";
break;
case TOUCH_TRIG:
wakeup_reason = "Touch";
break;
case SAR_TRIG:
wakeup_reason = "SAR";
break;
case BT_TRIG:
wakeup_reason = "BT";
break;
default:
wakeup_reason = "Unknown";
}
ESP_LOGD(TAG, "Wakeup Reason: %s", wakeup_reason);
device_info += "|Wakeup: "; device_info += "|Wakeup: ";
device_info += wakeup_reason; device_info += wakeup_reason;
} }

View File

@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import CONF_CPU_FREQUENCY
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLOCK, CONF_BLOCK,
@ -10,6 +11,7 @@ from esphome.const import (
ICON_COUNTER, ICON_COUNTER,
ICON_TIMER, ICON_TIMER,
UNIT_BYTES, UNIT_BYTES,
UNIT_HERTZ,
UNIT_MILLISECOND, UNIT_MILLISECOND,
UNIT_PERCENT, UNIT_PERCENT,
) )
@ -60,6 +62,14 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
), ),
cv.Optional(CONF_CPU_FREQUENCY): cv.All(
sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon="mdi:speedometer",
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
} }
@ -85,3 +95,7 @@ async def to_code(config):
if psram_conf := config.get(CONF_PSRAM): if psram_conf := config.get(CONF_PSRAM):
sens = await sensor.new_sensor(psram_conf) sens = await sensor.new_sensor(psram_conf)
cg.add(debug_component.set_psram_sensor(sens)) cg.add(debug_component.set_psram_sensor(sens))
if cpu_freq_conf := config.get(CONF_CPU_FREQUENCY):
sens = await sensor.new_sensor(cpu_freq_conf)
cg.add(debug_component.set_cpu_frequency_sensor(sens))

View File

@ -31,9 +31,12 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6)
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
#if !defined(USE_ESP32_VARIANT_ESP32H2)
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
#endif #endif
#endif
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
} }
@ -65,7 +68,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
} }
void DeepSleepComponent::deep_sleep_() { void DeepSleepComponent::deep_sleep_() {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
@ -84,6 +87,15 @@ void DeepSleepComponent::deep_sleep_() {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
} }
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32H2)
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
}
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);

View File

@ -17,7 +17,6 @@ from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_ICON, CONF_ICON,
CONF_ID,
CONF_INVERTED, CONF_INVERTED,
CONF_MAX_VALUE, CONF_MAX_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
@ -153,9 +152,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( climate.climate_schema(DemoClimate)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(DemoClimate),
cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True),
} }
) )
@ -183,9 +183,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( cover.cover_schema(DemoCover)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(DemoCover),
cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True),
} }
) )
@ -211,9 +212,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( fan.fan_schema(DemoFan)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan),
cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True),
} }
) )
@ -251,7 +253,9 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( light.light_schema(DemoLight, light.LightType.RGB)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight),
cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True),
@ -377,39 +381,33 @@ async def to_code(config):
await cg.register_component(var, conf) await cg.register_component(var, conf)
for conf in config[CONF_CLIMATES]: for conf in config[CONF_CLIMATES]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await climate.new_climate(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await climate.register_climate(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_COVERS]: for conf in config[CONF_COVERS]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await cover.new_cover(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await cover.register_cover(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_FANS]: for conf in config[CONF_FANS]:
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) var = await fan.new_fan(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await fan.register_fan(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_LIGHTS]: for conf in config[CONF_LIGHTS]:
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) var = await light.new_light(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await light.register_light(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_NUMBERS]: for conf in config[CONF_NUMBERS]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await number.new_number(
await cg.register_component(var, conf)
await number.register_number(
var,
conf, conf,
min_value=conf[CONF_MIN_VALUE], min_value=conf[CONF_MIN_VALUE],
max_value=conf[CONF_MAX_VALUE], max_value=conf[CONF_MAX_VALUE],
step=conf[CONF_STEP], step=conf[CONF_STEP],
) )
await cg.register_component(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_SENSORS]: for conf in config[CONF_SENSORS]:

View File

@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import switch from esphome.components import switch
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
from esphome.cpp_generator import MockObjClass
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
@ -26,32 +27,30 @@ Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
) )
_SWITCH_SCHEMA = (
def _switch_schema(class_: MockObjClass) -> cv.Schema:
return (
switch.switch_schema( switch.switch_schema(
class_,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
) )
.extend( .extend(
{ {
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
DfrobotSen0395Component
),
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
) )
CONFIG_SCHEMA = cv.typed_schema( CONFIG_SCHEMA = cv.typed_schema(
{ {
"sensor_active": _SWITCH_SCHEMA.extend( "sensor_active": _switch_schema(Sen0395PowerSwitch),
{cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} "turn_on_led": _switch_schema(Sen0395LedSwitch),
), "presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
"turn_on_led": _SWITCH_SCHEMA.extend( "start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
{cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)}
),
"presence_via_uart": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)}
),
"start_after_boot": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)}
),
} }
) )

View File

@ -6,7 +6,6 @@ from esphome.const import (
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION, CONF_CLOSE_DURATION,
CONF_CLOSE_ENDSTOP, CONF_CLOSE_ENDSTOP,
CONF_ID,
CONF_MAX_DURATION, CONF_MAX_DURATION,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
CONF_OPEN_DURATION, CONF_OPEN_DURATION,
@ -17,9 +16,10 @@ from esphome.const import (
endstop_ns = cg.esphome_ns.namespace("endstop") endstop_ns = cg.esphome_ns.namespace("endstop")
EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component) EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
cover.cover_schema(EndstopCover)
.extend(
{ {
cv.GenerateID(): cv.declare_id(EndstopCover),
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
@ -29,13 +29,14 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_MAX_DURATION): 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,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@ -37,6 +38,7 @@ from esphome.const import (
__version__, __version__,
) )
from esphome.core import CORE, HexInt, TimePeriod from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
@ -54,6 +56,12 @@ from .const import ( # noqa
KEY_SUBMODULES, KEY_SUBMODULES,
KEY_VARIANT, KEY_VARIANT,
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_FRIENDLY, VARIANT_FRIENDLY,
VARIANTS, VARIANTS,
) )
@ -70,7 +78,43 @@ CONF_RELEASE = "release"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies]
CPU_FREQUENCIES = {
VARIANT_ESP32: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C2: get_cpu_frequencies(80, 120),
VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
}
# Make sure not missed here if a new variant added.
assert all(v in CPU_FREQUENCIES for v in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
def set_core_data(config): def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
variant = config[CONF_VARIANT]
# if not specified in config, set to 160MHz if supported, the fastest otherwise
if cpu_frequency is None:
choices = CPU_FREQUENCIES[variant]
if "160MHZ" in choices:
cpu_frequency = "160MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
elif cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"Invalid CPU frequency '{cpu_frequency}' for {config[CONF_VARIANT]}",
path=[CONF_CPU_FREQUENCY],
)
CORE.data[KEY_ESP32] = {} CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32 CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
@ -83,6 +127,7 @@ def set_core_data(config):
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT]
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@ -251,7 +296,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 5) RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
@ -274,12 +319,15 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
# pioarduino versions that don't require a release number # pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases # List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 0),
cv.Version(5, 4, 1), cv.Version(5, 4, 1),
cv.Version(5, 4, 0), cv.Version(5, 4, 0),
cv.Version(5, 3, 3),
cv.Version(5, 3, 2), cv.Version(5, 3, 2),
cv.Version(5, 3, 1), cv.Version(5, 3, 1),
cv.Version(5, 3, 0), cv.Version(5, 3, 0),
cv.Version(5, 1, 5), cv.Version(5, 1, 5),
cv.Version(5, 1, 6),
] ]
@ -321,8 +369,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value): def _esp_idf_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(5, 1, 5), "https://github.com/espressif/esp-idf.git"), "dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 5), None), "latest": (cv.Version(5, 1, 6), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
} }
@ -550,11 +598,15 @@ FLASH_SIZES = [
] ]
CONF_FLASH_SIZE = "flash_size" CONF_FLASH_SIZE = "flash_size"
CONF_CPU_FREQUENCY = "cpu_frequency"
CONF_PARTITIONS = "partitions" CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_BOARD): cv.string_strict, cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
*FULL_CPU_FREQUENCIES, upper=True
),
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True *FLASH_SIZES, upper=True
), ),
@ -595,6 +647,7 @@ async def to_code(config):
os.path.join(os.path.dirname(__file__), "post_build.py.script"), os.path.join(os.path.dirname(__file__), "post_build.py.script"),
) )
freq = config[CONF_CPU_FREQUENCY][:-3]
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf") cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
@ -628,6 +681,9 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
# Set default CPU frequency
add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
add_extra_build_file( add_extra_build_file(
@ -693,6 +749,7 @@ async def to_code(config):
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
cg.add(RawExpression(f"setCpuFrequencyMhz({freq})"))
APP_PARTITION_SIZES = { APP_PARTITION_SIZES = {

View File

@ -13,11 +13,13 @@
#include <hal/cpu_hal.h> #include <hal/cpu_hal.h>
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <esp32-hal.h> #include <Esp.h>
#endif #else
#include <esp_clk_tree.h>
void setup(); void setup();
void loop(); void loop();
#endif
namespace esphome { namespace esphome {
@ -59,9 +61,13 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
#endif #endif
uint32_t arch_get_cpu_freq_hz() { uint32_t arch_get_cpu_freq_hz() {
rtc_cpu_freq_config_t config; uint32_t freq = 0;
rtc_clk_cpu_freq_get_config(&config); #ifdef USE_ESP_IDF
return config.freq_mhz * 1000000U; esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif
return freq;
} }
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF

View File

@ -2,42 +2,66 @@
#include "gpio.h" #include "gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "hal/gpio_hal.h"
#include "soc/soc_caps.h"
#include "soc/gpio_periph.h"
#include <cinttypes> #include <cinttypes>
#if (SOC_RTCIO_PIN_COUNT > 0)
#include "hal/rtc_io_hal.h"
#endif
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
#endif
namespace esphome { namespace esphome {
namespace esp32 { namespace esp32 {
static const char *const TAG = "esp32"; static const char *const TAG = "esp32";
static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)};
bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { static gpio_mode_t flags_to_mode(gpio::Flags flags) {
flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN));
if (flags == gpio::FLAG_INPUT) { if (flags == gpio::FLAG_INPUT)
return GPIO_MODE_INPUT; return GPIO_MODE_INPUT;
} else if (flags == gpio::FLAG_OUTPUT) { if (flags == gpio::FLAG_OUTPUT)
return GPIO_MODE_OUTPUT; return GPIO_MODE_OUTPUT;
} else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_OUTPUT_OD; return GPIO_MODE_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_INPUT_OUTPUT_OD; return GPIO_MODE_INPUT_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT))
return GPIO_MODE_INPUT_OUTPUT; return GPIO_MODE_INPUT_OUTPUT;
} else {
// unsupported or gpio::FLAG_NONE // unsupported or gpio::FLAG_NONE
return GPIO_MODE_DISABLE; return GPIO_MODE_DISABLE;
} }
}
struct ISRPinArg { struct ISRPinArg {
gpio_num_t pin; gpio_num_t pin;
gpio::Flags flags;
bool inverted; bool inverted;
#if defined(USE_ESP32_VARIANT_ESP32)
bool use_rtc;
int rtc_pin;
#endif
}; };
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_; arg->pin = this->pin_;
arg->flags = gpio::FLAG_NONE;
arg->inverted = inverted_; arg->inverted = inverted_;
#if defined(USE_ESP32_VARIANT_ESP32)
arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_);
if (arg->use_rtc)
arg->rtc_pin = rtc_io_number_get(this->pin_);
#endif
return ISRInternalGPIOPin((void *) arg); return ISRInternalGPIOPin((void *) arg);
} }
@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) { if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_); gpio_set_drive_capability(pin_, drive_strength_);
} }
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
} }
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
using namespace esp32; using namespace esp32;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
return bool(gpio_get_level(arg->pin)) != arg->inverted; return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
} }
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0); gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
} }
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
// not supported // not supported
} }
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
gpio_set_direction(arg->pin, flags_to_mode(flags)); gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags);
gpio_pull_mode_t pull_mode = GPIO_FLOATING; if (diff & gpio::FLAG_OUTPUT) {
if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { if (flags & gpio::FLAG_OUTPUT) {
pull_mode = GPIO_PULLUP_PULLDOWN; gpio_hal_output_enable(&GPIO_HAL, arg->pin);
} else if (flags & gpio::FLAG_PULLUP) { if (flags & gpio::FLAG_OPEN_DRAIN)
pull_mode = GPIO_PULLUP_ONLY; gpio_hal_od_enable(&GPIO_HAL, arg->pin);
} else if (flags & gpio::FLAG_PULLDOWN) { } else {
pull_mode = GPIO_PULLDOWN_ONLY; gpio_hal_output_disable(&GPIO_HAL, arg->pin);
} }
gpio_set_pull_mode(arg->pin, pull_mode); }
if (diff & gpio::FLAG_INPUT) {
if (flags & gpio::FLAG_INPUT) {
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
#if defined(USE_ESP32_VARIANT_ESP32)
if (arg->use_rtc) {
if (flags & gpio::FLAG_PULLUP) {
rtcio_hal_pullup_enable(arg->rtc_pin);
} else {
rtcio_hal_pullup_disable(arg->rtc_pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
rtcio_hal_pulldown_enable(arg->rtc_pin);
} else {
rtcio_hal_pulldown_disable(arg->rtc_pin);
}
} else
#endif
{
if (flags & gpio::FLAG_PULLUP) {
gpio_hal_pullup_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pullup_dis(&GPIO_HAL, arg->pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
gpio_hal_pulldown_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin);
}
}
} else {
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
}
}
arg->flags = flags;
} }
} // namespace esphome } // namespace esphome

View File

@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Any from typing import Any, Callable
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -64,8 +64,7 @@ def _lookup_pin(value):
def _translate_pin(value): def _translate_pin(value):
if isinstance(value, dict) or value is None: if isinstance(value, dict) or value is None:
raise cv.Invalid( raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas " "This variable only supports pin numbers, not full pin schemas (with inverted and mode)."
"(with inverted and mode)."
) )
if isinstance(value, int) and not isinstance(value, bool): if isinstance(value, int) and not isinstance(value, bool):
return value return value
@ -82,30 +81,22 @@ def _translate_pin(value):
@dataclass @dataclass
class ESP32ValidationFunctions: class ESP32ValidationFunctions:
pin_validation: Any pin_validation: Callable[[Any], Any]
usage_validation: Any usage_validation: Callable[[Any], Any]
_esp32_validations = { _esp32_validations = {
VARIANT_ESP32: ESP32ValidationFunctions( VARIANT_ESP32: ESP32ValidationFunctions(
pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports
), ),
VARIANT_ESP32S2: ESP32ValidationFunctions( VARIANT_ESP32C2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin, pin_validation=esp32_c2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports, usage_validation=esp32_c2_validate_supports,
), ),
VARIANT_ESP32C3: ESP32ValidationFunctions( VARIANT_ESP32C3: ESP32ValidationFunctions(
pin_validation=esp32_c3_validate_gpio_pin, pin_validation=esp32_c3_validate_gpio_pin,
usage_validation=esp32_c3_validate_supports, usage_validation=esp32_c3_validate_supports,
), ),
VARIANT_ESP32S3: ESP32ValidationFunctions(
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
),
VARIANT_ESP32C2: ESP32ValidationFunctions(
pin_validation=esp32_c2_validate_gpio_pin,
usage_validation=esp32_c2_validate_supports,
),
VARIANT_ESP32C6: ESP32ValidationFunctions( VARIANT_ESP32C6: ESP32ValidationFunctions(
pin_validation=esp32_c6_validate_gpio_pin, pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports, usage_validation=esp32_c6_validate_supports,
@ -114,6 +105,14 @@ _esp32_validations = {
pin_validation=esp32_h2_validate_gpio_pin, pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports, usage_validation=esp32_h2_validate_supports,
), ),
VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports,
),
VARIANT_ESP32S3: ESP32ValidationFunctions(
pin_validation=esp32_s3_validate_gpio_pin,
usage_validation=esp32_s3_validate_supports,
),
} }

View File

@ -31,8 +31,7 @@ def esp32_validate_gpio_pin(value):
) )
if 9 <= value <= 10: if 9 <= value <= 10:
_LOGGER.warning( _LOGGER.warning(
"Pin %s (9-10) might already be used by the " "Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.",
"flash interface in QUAD IO flash mode.",
value, value,
) )
if value in (24, 28, 29, 30, 31): if value in (24, 28, 29, 30, 31):

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