1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-29 00:22:21 +01:00

Merge branch 'dev' into sha256_ota

This commit is contained in:
J. Nick Koston
2025-09-25 16:03:42 -05:00
committed by GitHub
93 changed files with 1754 additions and 1485 deletions

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -17,12 +17,12 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -22,17 +22,17 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
- name: Auto Label PR
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.11"
@@ -47,7 +47,7 @@ jobs:
fi
- if: failure()
name: Review PR
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: generated-proto-files
path: |
@@ -70,7 +70,7 @@ jobs:
esphome/components/api/api_pb2_service.*
- if: success()
name: Dismiss review
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({

View File

@@ -20,10 +20,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.11"
@@ -41,7 +41,7 @@ jobs:
- if: failure()
name: Request changes
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -54,7 +54,7 @@ jobs:
- if: success()
name: Dismiss review
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({

View File

@@ -43,13 +43,13 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Set TAG
run: |

View File

@@ -36,18 +36,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -137,7 +137,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -157,12 +157,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.5.1
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@v4.2.4
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -180,7 +180,7 @@ jobs:
component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -215,15 +215,15 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python 3.13
id: python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -288,7 +288,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -301,14 +301,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -375,7 +375,7 @@ jobs:
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -401,7 +401,7 @@ jobs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Split components into 20 groups
id: split
run: |
@@ -431,7 +431,7 @@ jobs:
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -460,16 +460,16 @@ jobs:
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: pre-commit/action@v3.0.1
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
env:
SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@v1.1.0
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
ci-status:

View File

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Request reviews from component codeowners
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const owner = context.repo.owner;

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4
with:
category: "/language:${{matrix.language}}"

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Add external component comment
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const owner = context.repo.owner;

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.x"
- name: Build
@@ -70,7 +70,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.13.0
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
@@ -92,22 +92,22 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -168,27 +168,27 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -220,7 +220,7 @@ jobs:
- deploy-manifest
steps:
- name: Trigger Workflow
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: |
@@ -246,7 +246,7 @@ jobs:
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Trigger Workflow
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: |

View File

@@ -17,7 +17,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10.0.0
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with:
days-before-pr-stale: 90
days-before-pr-close: 7
@@ -37,7 +37,7 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10.0.0
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
with:
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@@ -16,7 +16,7 @@ jobs:
- merge-after-release
steps:
- name: Check for ${{ matrix.label }} label
uses: actions/github-script@v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout Home Assistant
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.1
rev: v0.13.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -534,6 +534,7 @@ esphome/components/wk2204_spi/* @DrCoolZic
esphome/components/wk2212_i2c/* @DrCoolZic
esphome/components/wk2212_spi/* @DrCoolZic
esphome/components/wl_134/* @hobbypunk90
esphome/components/wts01/* @alepee
esphome/components/x9c/* @EtienneMD
esphome/components/xgzp68xx/* @gcormier
esphome/components/xiaomi_hhccjcy10/* @fariouche

View File

@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args)
def command_clean_all(args: ArgsProtocol) -> int | None:
try:
writer.clean_all(args.configuration)
except OSError as err:
_LOGGER.error("Error cleaning all files: %s", err)
return 1
_LOGGER.info("Done!")
return 0
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import mqtt
@@ -921,6 +931,7 @@ PRE_CONFIG_ACTIONS = {
"dashboard": command_dashboard,
"vscode": command_vscode,
"update-all": command_update_all,
"clean-all": command_clean_all,
}
POST_CONFIG_ACTIONS = {
@@ -929,9 +940,9 @@ POST_CONFIG_ACTIONS = {
"upload": command_upload,
"logs": command_logs,
"run": command_run,
"clean": command_clean,
"clean-mqtt": command_clean_mqtt,
"mqtt-fingerprint": command_mqtt_fingerprint,
"clean": command_clean,
"idedata": command_idedata,
"rename": command_rename,
"discover": command_discover,
@@ -1144,6 +1155,11 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_clean_all = subparsers.add_parser("clean-all", help="Clean all files.")
parser_clean_all.add_argument(
"configuration", help="Your YAML configuration directory.", nargs="*"
)
parser_dashboard = subparsers.add_parser(
"dashboard", help="Create a simple web server for a dashboard."
)
@@ -1190,7 +1206,7 @@ def parse_args(argv):
parser_update = subparsers.add_parser("update-all")
parser_update.add_argument(
"configuration", help="Your YAML configuration file directories.", nargs="+"
"configuration", help="Your YAML configuration file or directory.", nargs="+"
)
parser_idedata = subparsers.add_parser("idedata")

View File

@@ -15,7 +15,10 @@ from esphome.const import (
CONF_TYPE_ID,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.util import Registry
@@ -49,11 +52,11 @@ def maybe_conf(conf, *validators):
return validate
def register_action(name, action_type, schema):
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
def register_condition(name, condition_type, schema):
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
return CONDITION_REGISTRY.register(name, condition_type, schema)
@@ -164,43 +167,78 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
@register_condition("and", AndCondition, validate_condition_list)
async def and_condition_to_code(config, condition_id, template_arg, args):
async def and_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("or", OrCondition, validate_condition_list)
async def or_condition_to_code(config, condition_id, template_arg, args):
async def or_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("all", AndCondition, validate_condition_list)
async def all_condition_to_code(config, condition_id, template_arg, args):
async def all_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("any", OrCondition, validate_condition_list)
async def any_condition_to_code(config, condition_id, template_arg, args):
async def any_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("not", NotCondition, validate_potentially_and_condition)
async def not_condition_to_code(config, condition_id, template_arg, args):
async def not_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
condition = await build_condition(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, condition)
@register_condition("xor", XorCondition, validate_condition_list)
async def xor_condition_to_code(config, condition_id, template_arg, args):
async def xor_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
async def lambda_condition_to_code(config, condition_id, template_arg, args):
async def lambda_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool)
return cg.new_Pvariable(condition_id, template_arg, lambda_)
@@ -217,7 +255,12 @@ async def lambda_condition_to_code(config, condition_id, template_arg, args):
}
).extend(cv.COMPONENT_SCHEMA),
)
async def for_condition_to_code(config, condition_id, template_arg, args):
async def for_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
condition = await build_condition(
config[CONF_CONDITION], cg.TemplateArguments(), []
)
@@ -231,7 +274,12 @@ async def for_condition_to_code(config, condition_id, template_arg, args):
@register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
)
async def delay_action_to_code(config, action_id, template_arg, args):
async def delay_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_component(var, {})
template_ = await cg.templatable(config, args, cg.uint32)
@@ -256,10 +304,15 @@ async def delay_action_to_code(config, action_id, template_arg, args):
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
)
async def if_action_to_code(config, action_id, template_arg, args):
async def if_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
conditions = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, conditions)
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
@@ -279,9 +332,14 @@ async def if_action_to_code(config, action_id, template_arg, args):
}
),
)
async def while_action_to_code(config, action_id, template_arg, args):
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, conditions)
async def while_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
return var
@@ -297,7 +355,12 @@ async def while_action_to_code(config, action_id, template_arg, args):
}
),
)
async def repeat_action_to_code(config, action_id, template_arg, args):
async def repeat_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32)
cg.add(var.set_count(count_template))
@@ -320,9 +383,14 @@ _validate_wait_until = cv.maybe_simple_value(
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code(config, action_id, template_arg, args):
conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, conditions)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
if CONF_TIMEOUT in config:
template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
cg.add(var.set_timeout_value(template_))
@@ -331,7 +399,12 @@ async def wait_until_action_to_code(config, action_id, template_arg, args):
@register_action("lambda", LambdaAction, cv.lambda_)
async def lambda_action_to_code(config, action_id, template_arg, args):
async def lambda_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)
@@ -345,7 +418,12 @@ async def lambda_action_to_code(config, action_id, template_arg, args):
}
),
)
async def component_update_action_to_code(config, action_id, template_arg, args):
async def component_update_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@@ -359,7 +437,12 @@ async def component_update_action_to_code(config, action_id, template_arg, args)
}
),
)
async def component_suspend_action_to_code(config, action_id, template_arg, args):
async def component_suspend_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@@ -376,7 +459,12 @@ async def component_suspend_action_to_code(config, action_id, template_arg, args
}
),
)
async def component_resume_action_to_code(config, action_id, template_arg, args):
async def component_resume_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
@@ -385,7 +473,9 @@ async def component_resume_action_to_code(config, action_id, template_arg, args)
return var
async def build_action(full_config, template_arg, args):
async def build_action(
full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
) -> MockObj:
registry_entry, config = cg.extract_registry_entry_config(
ACTION_REGISTRY, full_config
)
@@ -394,15 +484,19 @@ async def build_action(full_config, template_arg, args):
return await builder(config, action_id, template_arg, args)
async def build_action_list(config, templ, arg_type):
actions = []
async def build_action_list(
config: list[ConfigType], templ: cg.TemplateArguments, arg_type: TemplateArgsType
) -> list[MockObj]:
actions: list[MockObj] = []
for conf in config:
action = await build_action(conf, templ, arg_type)
actions.append(action)
return actions
async def build_condition(full_config, template_arg, args):
async def build_condition(
full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
) -> MockObj:
registry_entry, config = cg.extract_registry_entry_config(
CONDITION_REGISTRY, full_config
)
@@ -411,15 +505,19 @@ async def build_condition(full_config, template_arg, args):
return await builder(config, action_id, template_arg, args)
async def build_condition_list(config, templ, args):
conditions = []
async def build_condition_list(
config: ConfigType, templ: cg.TemplateArguments, args: TemplateArgsType
) -> list[MockObj]:
conditions: list[MockObj] = []
for conf in config:
condition = await build_condition(conf, templ, args)
conditions.append(condition)
return conditions
async def build_automation(trigger, args, config):
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)

View File

@@ -1,4 +1,5 @@
import base64
import logging
from esphome import automation
from esphome.automation import Condition
@@ -25,6 +26,9 @@ from esphome.const import (
CONF_VARIABLES,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "api"
DEPENDENCIES = ["network"]
@@ -101,6 +105,32 @@ def _encryption_schema(config):
return ENCRYPTION_SCHEMA(config)
def _validate_api_config(config: ConfigType) -> ConfigType:
"""Validate API configuration with mutual exclusivity check and deprecation warning."""
# Check if both password and encryption are configured
has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
has_encryption = CONF_ENCRYPTION in config
if has_password and has_encryption:
raise cv.Invalid(
"The 'password' and 'encryption' options are mutually exclusive. "
"The API client only supports one authentication method at a time. "
"Please remove one of them. "
"Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
"We strongly recommend using 'encryption' instead for better security."
)
# Warn about password deprecation
if has_password:
_LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api.html#configuration-variables"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -131,6 +161,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_api_config,
)

View File

@@ -102,7 +102,7 @@ message HelloRequest {
// For example "Home Assistant"
// Not strictly necessary to send but nice for debugging
// purposes.
string client_info = 1;
string client_info = 1 [(pointer_to_buffer) = true];
uint32 api_version_major = 2;
uint32 api_version_minor = 3;
}
@@ -139,7 +139,7 @@ message AuthenticationRequest {
option (ifdef) = "USE_API_PASSWORD";
// The password to log in with
string password = 1;
string password = 1 [(pointer_to_buffer) = true];
}
// Confirmation of successful connection. After this the connection is available for all traffic.
@@ -769,7 +769,7 @@ message HomeassistantServiceMap {
string value = 2 [(no_zero_copy) = true];
}
message HomeassistantServiceResponse {
message HomeassistantActionRequest {
option (id) = 35;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
@@ -824,7 +824,7 @@ message GetTimeResponse {
option (no_delay) = true;
fixed32 epoch_seconds = 1;
string timezone = 2;
string timezone = 2 [(pointer_to_buffer) = true];
}
// ==================== USER-DEFINES SERVICES ====================
@@ -1465,7 +1465,7 @@ message BluetoothDeviceRequest {
uint64 address = 1;
BluetoothDeviceRequestType request_type = 2;
bool has_address_type = 3;
bool has_address_type = 3; // Deprecated, should be removed in 2027.8 - https://github.com/esphome/esphome/pull/10318
uint32 address_type = 4;
}
@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
uint32 handle = 2;
bool response = 3;
bytes data = 4;
bytes data = 4 [(pointer_to_buffer) = true];
}
message BluetoothGATTReadDescriptorRequest {
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
uint64 address = 1;
uint32 handle = 2;
bytes data = 3;
bytes data = 3 [(pointer_to_buffer) = true];
}
message BluetoothGATTNotifyRequest {
@@ -1865,10 +1865,22 @@ message VoiceAssistantWakeWord {
repeated string trained_languages = 3;
}
message VoiceAssistantExternalWakeWord {
string id = 1;
string wake_word = 2;
repeated string trained_languages = 3;
string model_type = 4;
uint32 model_size = 5;
string model_hash = 6;
string url = 7;
}
message VoiceAssistantConfigurationRequest {
option (id) = 121;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantExternalWakeWord external_wake_words = 1;
}
message VoiceAssistantConfigurationResponse {
@@ -2292,7 +2304,7 @@ message ZWaveProxyFrame {
option (ifdef) = "USE_ZWAVE_PROXY";
option (no_delay) = true;
bytes data = 1 [(fixed_array_size) = 257];
bytes data = 1 [(pointer_to_buffer) = true];
}
enum ZWaveProxyRequestType {

View File

@@ -1078,8 +1078,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) {
homeassistant::global_homeassistant_time->set_timezone(value.timezone);
if (value.timezone_len > 0) {
const std::string &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
}
#endif
}
@@ -1196,6 +1202,23 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
resp_wake_word.trained_languages.push_back(lang);
}
}
// Filter external wake words
for (auto &wake_word : msg.external_wake_words) {
if (wake_word.model_type != "micro") {
// microWakeWord only
continue;
}
resp.available_wake_words.emplace_back();
auto &resp_wake_word = resp.available_wake_words.back();
resp_wake_word.set_id(StringRef(wake_word.id));
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang);
}
}
resp.active_wake_words = &config.active_wake_words;
resp.max_active_wake_words = config.max_active_wake_words;
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
@@ -1374,7 +1397,7 @@ void APIConnection::complete_authentication_() {
}
bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_.name = msg.client_info;
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
this->client_info_.peername = this->helper_->getpeername();
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
@@ -1402,7 +1425,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
AuthenticationResponse resp;
// bool invalid_password = 1;
resp.invalid_password = !this->parent_->check_password(msg.password);
resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
if (!resp.invalid_password) {
this->complete_authentication_();
}

View File

@@ -10,8 +10,8 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include <vector>
#include <functional>
#include <vector>
namespace esphome::api {
@@ -132,10 +132,10 @@ class APIConnection final : public APIServerConnection {
#endif
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
void send_homeassistant_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription)
return;
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
}
#endif
#ifdef USE_BLUETOOTH_PROXY

View File

@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011;
// pointer_to_buffer: Use pointer instead of array for fixed-size byte fields
// When set, the field will be declared as a pointer (const uint8_t *data)
// instead of an array (uint8_t data[N]). This allows zero-copy on decode
// by pointing directly to the protobuf buffer. The buffer must remain valid
// until the message is processed (which is guaranteed for stack-allocated messages).
optional bool pointer_to_buffer = 50012 [default=false];
// container_pointer: Zero-copy optimization for repeated fields.
//
// When container_pointer is set on a repeated field, the generated message will

View File

@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->client_info = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->client_info = value.data();
this->client_info_len = value.size();
break;
}
default:
return false;
}
@@ -45,9 +48,12 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_PASSWORD
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->password = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->password = value.data();
this->password_len = value.size();
break;
}
default:
return false;
}
@@ -866,7 +872,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
size.add_length(1, this->key_ref_.size());
size.add_length(1, this->value.size());
}
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) {
buffer.encode_message(2, it, true);
@@ -879,7 +885,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
}
buffer.encode_bool(5, this->is_event);
}
void HomeassistantServiceResponse::calculate_size(ProtoSize &size) const {
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size());
size.add_repeated_message(1, this->data);
size.add_repeated_message(1, this->data_template);
@@ -917,9 +923,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
#endif
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
this->timezone = value.as_string();
case 2: {
// Use raw data directly to avoid allocation
this->timezone = value.data();
this->timezone_len = value.size();
break;
}
default:
return false;
}
@@ -2028,9 +2037,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
}
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 4:
this->data = value.as_string();
case 4: {
// Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break;
}
default:
return false;
}
@@ -2064,9 +2076,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
}
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3:
this->data = value.as_string();
case 3: {
// Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break;
}
default:
return false;
}
@@ -2382,6 +2397,52 @@ void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const {
}
}
}
bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 5:
this->model_size = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->id = value.as_string();
break;
case 2:
this->wake_word = value.as_string();
break;
case 3:
this->trained_languages.push_back(value.as_string());
break;
case 4:
this->model_type = value.as_string();
break;
case 6:
this->model_hash = value.as_string();
break;
case 7:
this->url = value.as_string();
break;
default:
return false;
}
return true;
}
bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->external_wake_words.emplace_back();
value.decode_to_message(this->external_wake_words.back());
break;
default:
return false;
}
return true;
}
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) {
buffer.encode_message(1, it, true);
@@ -3029,12 +3090,9 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
const std::string &data_str = value.as_string();
this->data_len = data_str.size();
if (this->data_len > 257) {
this->data_len = 257;
}
memcpy(this->data, data_str.data(), this->data_len);
// Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break;
}
default:

View File

@@ -330,11 +330,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
class HelloRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 1;
static constexpr uint8_t ESTIMATED_SIZE = 17;
static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "hello_request"; }
#endif
std::string client_info{};
const uint8_t *client_info{nullptr};
uint16_t client_info_len{0};
uint32_t api_version_major{0};
uint32_t api_version_minor{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -370,11 +371,12 @@ class HelloResponse final : public ProtoMessage {
class AuthenticationRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 3;
static constexpr uint8_t ESTIMATED_SIZE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_request"; }
#endif
std::string password{};
const uint8_t *password{nullptr};
uint16_t password_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1098,12 +1100,12 @@ class HomeassistantServiceMap final : public ProtoMessage {
protected:
};
class HomeassistantServiceResponse final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 113;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_service_response"; }
const char *message_name() const override { return "homeassistant_action_request"; }
#endif
StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
@@ -1188,12 +1190,13 @@ class GetTimeRequest final : public ProtoMessage {
class GetTimeResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 14;
static constexpr uint8_t ESTIMATED_SIZE = 24;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "get_time_response"; }
#endif
uint32_t epoch_seconds{0};
std::string timezone{};
const uint8_t *timezone{nullptr};
uint16_t timezone_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1985,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 75;
static constexpr uint8_t ESTIMATED_SIZE = 19;
static constexpr uint8_t ESTIMATED_SIZE = 29;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
#endif
uint64_t address{0};
uint32_t handle{0};
bool response{false};
std::string data{};
const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2020,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 77;
static constexpr uint8_t ESTIMATED_SIZE = 17;
static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
#endif
uint64_t address{0};
uint32_t handle{0};
std::string data{};
const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2451,18 +2456,37 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
protected:
};
class VoiceAssistantConfigurationRequest final : public ProtoMessage {
class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 121;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "voice_assistant_configuration_request"; }
#endif
std::string id{};
std::string wake_word{};
std::vector<std::string> trained_languages{};
std::string model_type{};
uint32_t model_size{0};
std::string model_hash{};
std::string url{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 121;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "voice_assistant_configuration_request"; }
#endif
std::vector<VoiceAssistantExternalWakeWord> external_wake_words{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class VoiceAssistantConfigurationResponse final : public ProtoMessage {
public:
@@ -2929,11 +2953,11 @@ class UpdateCommandRequest final : public CommandProtoMessage {
class ZWaveProxyFrame final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 128;
static constexpr uint8_t ESTIMATED_SIZE = 33;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "z_wave_proxy_frame"; }
#endif
uint8_t data[257]{};
const uint8_t *data{nullptr};
uint16_t data_len{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;

View File

@@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
void HelloRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HelloRequest");
dump_field(out, "client_info", this->client_info);
out.append(" client_info: ");
out.append(format_hex_pretty(this->client_info, this->client_info_len));
out.append("\n");
dump_field(out, "api_version_major", this->api_version_major);
dump_field(out, "api_version_minor", this->api_version_minor);
}
@@ -682,7 +684,12 @@ void HelloResponse::dump_to(std::string &out) const {
dump_field(out, "name", this->name_ref_);
}
#ifdef USE_API_PASSWORD
void AuthenticationRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); }
void AuthenticationRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationRequest");
out.append(" password: ");
out.append(format_hex_pretty(this->password, this->password_len));
out.append("\n");
}
void AuthenticationResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationResponse");
dump_field(out, "invalid_password", this->invalid_password);
@@ -1094,8 +1101,8 @@ void HomeassistantServiceMap::dump_to(std::string &out) const {
dump_field(out, "key", this->key_ref_);
dump_field(out, "value", this->value);
}
void HomeassistantServiceResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceResponse");
void HomeassistantActionRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantActionRequest");
dump_field(out, "service", this->service_ref_);
for (const auto &it : this->data) {
out.append(" data: ");
@@ -1136,7 +1143,9 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
void GetTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds);
dump_field(out, "timezone", this->timezone);
out.append(" timezone: ");
out.append(format_hex_pretty(this->timezone, this->timezone_len));
out.append("\n");
}
#ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
@@ -1649,7 +1658,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
dump_field(out, "handle", this->handle);
dump_field(out, "response", this->response);
out.append(" data: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
@@ -1662,7 +1671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle);
out.append(" data: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
@@ -1815,8 +1824,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const {
dump_field(out, "trained_languages", it, 4);
}
}
void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord");
dump_field(out, "id", this->id);
dump_field(out, "wake_word", this->wake_word);
for (const auto &it : this->trained_languages) {
dump_field(out, "trained_languages", it, 4);
}
dump_field(out, "model_type", this->model_type);
dump_field(out, "model_size", this->model_size);
dump_field(out, "model_hash", this->model_hash);
dump_field(out, "url", this->url);
}
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
out.append("VoiceAssistantConfigurationRequest {}");
MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest");
for (const auto &it : this->external_wake_words) {
out.append(" external_wake_words: ");
it.dump_to(out);
out.append("\n");
}
}
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse");

View File

@@ -548,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_VOICE_ASSISTANT
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
VoiceAssistantConfigurationRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif
@@ -639,241 +639,139 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) {
}
}
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (this->check_connection_setup_() && !this->send_device_info_response(msg)) {
if (!this->send_device_info_response(msg)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
if (this->check_authenticated_()) {
this->list_entities(msg);
}
}
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_states(msg);
}
}
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_logs(msg);
}
this->subscribe_states(msg);
}
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServerConnection::on_subscribe_homeassistant_services_request(
const SubscribeHomeassistantServicesRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_homeassistant_services(msg);
}
this->subscribe_homeassistant_services(msg);
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_home_assistant_states(msg);
}
this->subscribe_home_assistant_states(msg);
}
#endif
#ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
if (this->check_authenticated_()) {
this->execute_service(msg);
}
}
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
#endif
#ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) {
if (!this->send_noise_encryption_set_key_response(msg)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (this->check_authenticated_()) {
this->button_command(msg);
}
}
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
#endif
#ifdef USE_CAMERA
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
if (this->check_authenticated_()) {
this->camera_image(msg);
}
}
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
#endif
#ifdef USE_CLIMATE
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
if (this->check_authenticated_()) {
this->climate_command(msg);
}
}
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
#endif
#ifdef USE_COVER
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
if (this->check_authenticated_()) {
this->cover_command(msg);
}
}
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
#endif
#ifdef USE_DATETIME_DATE
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) {
if (this->check_authenticated_()) {
this->date_command(msg);
}
}
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
#endif
#ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
if (this->check_authenticated_()) {
this->datetime_command(msg);
}
this->datetime_command(msg);
}
#endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
if (this->check_authenticated_()) {
this->fan_command(msg);
}
}
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (this->check_authenticated_()) {
this->light_command(msg);
}
}
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (this->check_authenticated_()) {
this->lock_command(msg);
}
}
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (this->check_authenticated_()) {
this->media_player_command(msg);
}
this->media_player_command(msg);
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
if (this->check_authenticated_()) {
this->number_command(msg);
}
}
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (this->check_authenticated_()) {
this->select_command(msg);
}
}
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
#endif
#ifdef USE_SIREN
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
if (this->check_authenticated_()) {
this->siren_command(msg);
}
}
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (this->check_authenticated_()) {
this->switch_command(msg);
}
}
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (this->check_authenticated_()) {
this->text_command(msg);
}
}
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
#endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (this->check_authenticated_()) {
this->time_command(msg);
}
}
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
#endif
#ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
if (this->check_authenticated_()) {
this->update_command(msg);
}
}
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
#endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (this->check_authenticated_()) {
this->valve_command(msg);
}
}
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_bluetooth_le_advertisements(msg);
}
this->subscribe_bluetooth_le_advertisements(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_device_request(msg);
}
this->bluetooth_device_request(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_get_services(msg);
}
this->bluetooth_gatt_get_services(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_read(msg);
}
this->bluetooth_gatt_read(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_write(msg);
}
this->bluetooth_gatt_write(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_read_descriptor(msg);
}
this->bluetooth_gatt_read_descriptor(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_write_descriptor(msg);
}
this->bluetooth_gatt_write_descriptor(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_gatt_notify(msg);
}
this->bluetooth_gatt_notify(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) {
if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
this->on_fatal_error();
}
}
@@ -881,59 +779,68 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
if (this->check_authenticated_()) {
this->unsubscribe_bluetooth_le_advertisements(msg);
}
this->unsubscribe_bluetooth_le_advertisements(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
if (this->check_authenticated_()) {
this->bluetooth_scanner_set_mode(msg);
}
this->bluetooth_scanner_set_mode(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_voice_assistant(msg);
}
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) {
if (!this->send_voice_assistant_get_configuration_response(msg)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (this->check_authenticated_()) {
this->voice_assistant_set_configuration(msg);
}
this->voice_assistant_set_configuration(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (this->check_authenticated_()) {
this->alarm_control_panel_command(msg);
}
this->alarm_control_panel_command(msg);
}
#endif
#ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
if (this->check_authenticated_()) {
this->zwave_proxy_frame(msg);
}
}
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
#endif
#ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
if (this->check_authenticated_()) {
this->zwave_proxy_request(msg);
}
}
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: // No setup required
#endif
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return; // Connection not setup
}
break;
default:
// All other messages require authentication (which includes connection check)
if (!this->check_authenticated_()) {
return; // Authentication failed
}
break;
}
// Call base implementation to process the message
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
}
} // namespace esphome::api

View File

@@ -477,6 +477,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
};
} // namespace esphome::api

View File

@@ -217,12 +217,12 @@ void APIServer::dump_config() {
}
#ifdef USE_API_PASSWORD
bool APIServer::check_password(const std::string &password) const {
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
// depend only on input password length
const char *a = this->password_.c_str();
uint32_t len_a = this->password_.length();
const char *b = password.c_str();
uint32_t len_b = password.length();
const char *b = reinterpret_cast<const char *>(password_data);
uint32_t len_b = password_len;
// disable optimization with volatile
volatile uint32_t length = len_b;
@@ -245,6 +245,7 @@ bool APIServer::check_password(const std::string &password) const {
return result == 0;
}
#endif
void APIServer::handle_disconnect(APIConnection *conn) {}
@@ -370,9 +371,9 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call);
client->send_homeassistant_action(call);
}
}
#endif

View File

@@ -37,7 +37,7 @@ class APIServer : public Component, public Controller {
void on_shutdown() override;
bool teardown() override;
#ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const;
bool check_password(const uint8_t *password_data, size_t password_len) const;
void set_password(const std::string &password);
#endif
void set_port(uint16_t port);
@@ -107,7 +107,8 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void send_homeassistant_action(const HomeassistantActionRequest &call);
#endif
#ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@@ -179,9 +179,9 @@ class CustomAPIDevice {
* @param service_name The service to call.
*/
void call_homeassistant_service(const std::string &service_name) {
HomeassistantServiceResponse resp;
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_service_call(resp);
global_api_server->send_homeassistant_action(resp);
}
/** Call a Home Assistant service from ESPHome.
@@ -199,7 +199,7 @@ class CustomAPIDevice {
* @param data The data for the service call, mapping from string to string.
*/
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp;
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
for (auto &it : data) {
resp.data.emplace_back();
@@ -207,7 +207,7 @@ class CustomAPIDevice {
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
global_api_server->send_homeassistant_service_call(resp);
global_api_server->send_homeassistant_action(resp);
}
/** Fire an ESPHome event in Home Assistant.
@@ -221,10 +221,10 @@ class CustomAPIDevice {
* @param event_name The event to fire.
*/
void fire_homeassistant_event(const std::string &event_name) {
HomeassistantServiceResponse resp;
HomeassistantActionRequest resp;
resp.set_service(StringRef(event_name));
resp.is_event = true;
global_api_server->send_homeassistant_service_call(resp);
global_api_server->send_homeassistant_action(resp);
}
/** Fire an ESPHome event in Home Assistant.
@@ -241,7 +241,7 @@ class CustomAPIDevice {
* @param data The data for the event, mapping from string to string.
*/
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp;
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
resp.is_event = true;
for (auto &it : data) {
@@ -250,7 +250,7 @@ class CustomAPIDevice {
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
global_api_server->send_homeassistant_service_call(resp);
global_api_server->send_homeassistant_action(resp);
}
#else
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {

View File

@@ -3,10 +3,10 @@
#include "api_server.h"
#ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include <vector>
#include "api_pb2.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include <vector>
namespace esphome::api {
@@ -62,7 +62,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
void play(Ts... x) override {
HomeassistantServiceResponse resp;
HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_;
@@ -84,7 +84,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
this->parent_->send_homeassistant_service_call(resp);
this->parent_->send_homeassistant_action(resp);
}
protected:

View File

@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
// Direct access to raw data without string allocation
const uint8_t *data() const { return this->value_; }
size_t size() const { return this->length_; }
/**
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
*
@@ -827,7 +831,7 @@ class ProtoService {
}
// Authentication helper methods
bool check_connection_setup_() {
inline bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
@@ -835,7 +839,7 @@ class ProtoService {
return true;
}
bool check_authenticated_() {
inline bool check_authenticated_() {
#ifdef USE_API_PASSWORD
if (!this->check_connection_setup_()) {
return false;

View File

@@ -6,8 +6,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID
from esphome.core import CORE
from esphome.log import AnsiFore, color
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"]
@@ -48,26 +46,6 @@ def validate_connections(config):
config
)
# Warn about connection slot waste when using Arduino framework
if CORE.using_arduino and connection_slots:
_LOGGER.warning(
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
"\n"
"To switch to ESP-IDF, add this to your YAML:\n"
" esp32:\n"
" framework:\n"
" type: esp-idf\n"
"\n"
"For detailed migration instructions, see:\n"
"%s",
color(
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
),
)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
@@ -81,19 +59,17 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
),
cv.Optional(CONF_CACHE_SERVICES, default=True): cv.boolean,
cv.Optional(
CONF_CONNECTION_SLOTS,
default=DEFAULT_CONNECTION_SLOTS,
): cv.All(
cv.positive_int,
cv.Range(min=1, max=esp32_ble_tracker.max_connections()),
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
),
cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=esp32_ble_tracker.max_connections()),
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
),
}
)

View File

@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
}
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED;
@@ -522,8 +523,11 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err =
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
}
@@ -540,7 +544,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
}
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "descriptor");
return ESP_GATT_NOT_CONNECTED;
@@ -548,8 +552,11 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err = esp_ble_gattc_write_char_descr(
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
}

View File

@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
esp_err_t read_characteristic(uint16_t handle);
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t read_descriptor(uint16_t handle);
esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response);
esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t notify_characteristic(uint16_t handle, bool enable);

View File

@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
return;
}
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err);
}
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
return;
}
auto err = connection->write_descriptor(msg.handle, msg.data, true);
auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err);
}

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@DT-art1"]
@@ -51,9 +50,8 @@ async def to_code(config: ConfigType) -> None:
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_QUALITY],

View File

@@ -1,3 +1,5 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
#include "esp32_camera_jpeg_encoder.h"
@@ -15,7 +17,7 @@ camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSp
this->bytes_written_ = 0;
this->out_of_output_memory_ = false;
bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height,
to_internal_(spec->format), this->quality_, callback_, this);
to_internal_(spec->format), this->quality_, callback, this);
if (!success)
return camera::ENCODER_ERROR_CONFIGURATION;
@@ -49,7 +51,7 @@ void ESP32CameraJPEGEncoder::dump_config() {
this->output_->get_max_size(), this->quality_, this->buffer_expand_size_);
}
size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) {
size_t ESP32CameraJPEGEncoder::callback(void *arg, size_t index, const void *data, size_t len) {
ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg);
uint8_t *buffer = that->output_->get_data();
size_t buffer_length = that->output_->get_max_size();

View File

@@ -1,5 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
#include <esp_camera.h>
@@ -24,7 +26,7 @@ class ESP32CameraJPEGEncoder : public camera::Encoder {
void dump_config() override;
// -------------------------
protected:
static size_t callback_(void *arg, size_t index, const void *data, size_t len);
static size_t callback(void *arg, size_t index, const void *data, size_t len);
pixformat_t to_internal_(camera::PixelFormat format);
camera::EncoderBuffer *output_{};

View File

@@ -174,16 +174,12 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds,
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
),
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
cv.only_with_esp_idf,
cv.Optional(CONF_DISABLE_BT_LOGS, default=True): cv.boolean,
cv.Optional(CONF_CONNECTION_TIMEOUT, default="20s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
),
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
cv.only_with_esp_idf,
cv.Optional(CONF_MAX_NOTIFICATIONS, default=12): cv.All(
cv.positive_int,
cv.Range(min=1, max=64),
),

View File

@@ -150,10 +150,6 @@ def as_reversed_hex_array(value):
)
def max_connections() -> int:
return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
@@ -172,7 +168,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=0, max=max_connections())
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
cv.Schema(
@@ -238,9 +234,8 @@ def validate_remaining_connections(config):
if used_slots <= config[CONF_MAX_CONNECTIONS]:
return config
slot_users = ", ".join(slots)
hard_limit = max_connections()
if used_slots < hard_limit:
if used_slots < IDF_MAX_CONNECTIONS:
_LOGGER.warning(
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
"connection slot(s) out of available configured maximum %d connection "
@@ -262,9 +257,9 @@ def validate_remaining_connections(config):
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
)
if config[CONF_MAX_CONNECTIONS] < hard_limit:
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {hard_limit} connection slot(s) limit."
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
raise cv.Invalid(msg)

View File

@@ -128,4 +128,4 @@ async def to_code(config):
cg.add_library("tonia/HeatpumpIR", "1.0.37")
if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

@@ -87,7 +87,7 @@ void HomeassistantNumber::control(float value) {
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantServiceResponse resp;
api::HomeassistantActionRequest resp;
resp.set_service(SERVICE_NAME);
resp.data.emplace_back();
@@ -100,7 +100,7 @@ void HomeassistantNumber::control(float value) {
entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value);
api::global_api_server->send_homeassistant_service_call(resp);
api::global_api_server->send_homeassistant_action(resp);
}
} // namespace homeassistant

View File

@@ -44,7 +44,7 @@ void HomeassistantSwitch::write_state(bool state) {
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
api::HomeassistantServiceResponse resp;
api::HomeassistantActionRequest resp;
if (state) {
resp.set_service(SERVICE_ON);
} else {
@@ -56,7 +56,7 @@ void HomeassistantSwitch::write_state(bool state) {
entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_;
api::global_api_server->send_homeassistant_service_call(resp);
api::global_api_server->send_homeassistant_action(resp);
}
} // namespace homeassistant

View File

@@ -9,8 +9,8 @@ static const char *const TAG = "htu21d";
static const uint8_t HTU21D_ADDRESS = 0x40;
static const uint8_t HTU21D_REGISTER_RESET = 0xFE;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5;
static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */
static const uint8_t HTU21D_REGISTER_STATUS = 0xE7;
static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */
@@ -57,7 +57,6 @@ void HTU21DComponent::update() {
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
this->status_clear_warning();
if (this->write(&HTU21D_REGISTER_HUMIDITY, 1) != i2c::ERROR_OK) {
this->status_set_warning();
@@ -79,10 +78,11 @@ void HTU21DComponent::update() {
if (this->humidity_ != nullptr)
this->humidity_->publish_state(humidity);
int8_t heater_level;
this->status_clear_warning();
// HTU21D does have a heater module but does not have heater level
// Setting heater level to 1 in case the heater is ON
uint8_t heater_level = 0;
if (this->sensor_model_ == HTU21D_SENSOR_MODEL_HTU21D) {
if (this->is_heater_enabled()) {
heater_level = 1;
@@ -97,34 +97,30 @@ void HTU21DComponent::update() {
if (this->heater_ != nullptr)
this->heater_->publish_state(heater_level);
this->status_clear_warning();
});
});
}
bool HTU21DComponent::is_heater_enabled() {
uint8_t raw_heater;
if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast<uint8_t *>(&raw_heater), 2) != i2c::ERROR_OK) {
if (this->read_register(HTU21D_REGISTER_STATUS, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return false;
}
raw_heater = i2c::i2ctohs(raw_heater);
return (bool) (((raw_heater) >> (HTU21D_REG_HTRE_BIT)) & 0x01);
return (bool) ((raw_heater >> HTU21D_REG_HTRE_BIT) & 0x01);
}
void HTU21DComponent::set_heater(bool status) {
uint8_t raw_heater;
if (this->read_register(HTU21D_REGISTER_STATUS, reinterpret_cast<uint8_t *>(&raw_heater), 2) != i2c::ERROR_OK) {
if (this->read_register(HTU21D_REGISTER_STATUS, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
raw_heater = i2c::i2ctohs(raw_heater);
if (status) {
raw_heater |= (1 << (HTU21D_REG_HTRE_BIT));
raw_heater |= (1 << HTU21D_REG_HTRE_BIT);
} else {
raw_heater &= ~(1 << (HTU21D_REG_HTRE_BIT));
raw_heater &= ~(1 << HTU21D_REG_HTRE_BIT);
}
if (this->write_register(HTU21D_WRITERHT_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
@@ -138,14 +134,13 @@ void HTU21DComponent::set_heater_level(uint8_t level) {
}
}
int8_t HTU21DComponent::get_heater_level() {
int8_t raw_heater;
if (this->read_register(HTU21D_READHEATER_REG_CMD, reinterpret_cast<uint8_t *>(&raw_heater), 2) != i2c::ERROR_OK) {
uint8_t HTU21DComponent::get_heater_level() {
uint8_t raw_heater;
if (this->read_register(HTU21D_READHEATER_REG_CMD, &raw_heater, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return 0;
}
raw_heater = i2c::i2ctohs(raw_heater);
return raw_heater;
return raw_heater & 0xF;
}
float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; }

View File

@@ -26,7 +26,7 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice {
bool is_heater_enabled();
void set_heater(bool status);
void set_heater_level(uint8_t level);
int8_t get_heater_level();
uint8_t get_heater_level();
float get_setup_priority() const override;

View File

@@ -19,6 +19,15 @@ std::string build_json(const json_build_t &f) {
bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(data);
if (doc.overflowed() || doc.isNull())
return false;
return f(doc.as<JsonObject>());
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
JsonDocument parse_json(const std::string &data) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
#ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
@@ -27,20 +36,18 @@ bool parse_json(const std::string &data, const json_parse_t &f) {
#endif
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return false;
return JsonObject(); // return unbound object
}
DeserializationError err = deserializeJson(json_document, data);
JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) {
return f(root);
return json_document;
} else if (err == DeserializationError::NoMemory) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return false;
return JsonObject(); // return unbound object
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
return JsonObject(); // return unbound object
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}

View File

@@ -49,6 +49,8 @@ std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const std::string &data);
/// Builder class for creating JSON documents without lambdas
class JsonBuilder {

View File

@@ -7,6 +7,7 @@ wave_4_3 = DriverChip(
"ESP32-S3-TOUCH-LCD-4.3",
swap_xy=UNDEFINED,
initsequence=(),
color_order="RGB",
width=800,
height=480,
pclk_frequency="16MHz",

View File

@@ -128,21 +128,21 @@ void MMC5603Component::update() {
raw_x |= buffer[1] << 4;
raw_x |= buffer[2] << 0;
const float x = 0.0625 * (raw_x - 524288);
const float x = 0.00625 * (raw_x - 524288);
int32_t raw_y = 0;
raw_y |= buffer[3] << 12;
raw_y |= buffer[4] << 4;
raw_y |= buffer[5] << 0;
const float y = 0.0625 * (raw_y - 524288);
const float y = 0.00625 * (raw_y - 524288);
int32_t raw_z = 0;
raw_z |= buffer[6] << 12;
raw_z |= buffer[7] << 4;
raw_z |= buffer[8] << 0;
const float z = 0.0625 * (raw_z - 524288);
const float z = 0.00625 * (raw_z - 524288);
const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading);

View File

@@ -66,7 +66,7 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure,
cv.Optional(CONF_TEMPERATURE_OFFSET): cv.All(
cv.temperature,
cv.temperature_delta,
cv.float_range(min=0, max=655.35),
),
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All(

View File

@@ -5,7 +5,10 @@
#include "esphome/core/component.h"
#include <vector>
#include "usb/usb_host.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include <list>
namespace esphome {
@@ -13,6 +16,10 @@ namespace usb_host {
static const char *const TAG = "usb_host";
// Forward declarations
struct TransferRequest;
class USBClient;
// constants for setup packet type
static const uint8_t USB_RECIP_DEVICE = 0;
static const uint8_t USB_RECIP_INTERFACE = 1;
@@ -25,7 +32,10 @@ static const uint8_t USB_DIR_IN = 1 << 7;
static const uint8_t USB_DIR_OUT = 0;
static const size_t SETUP_PACKET_SIZE = 8;
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5)
// used to report a transfer status
struct TransferStatus {
@@ -49,6 +59,31 @@ struct TransferRequest {
USBClient *client;
};
enum EventType : uint8_t {
EVENT_DEVICE_NEW,
EVENT_DEVICE_GONE,
EVENT_TRANSFER_COMPLETE,
EVENT_CONTROL_COMPLETE,
};
struct UsbEvent {
EventType type;
union {
struct {
uint8_t address;
} device_new;
struct {
usb_device_handle_t handle;
} device_gone;
struct {
TransferRequest *trq;
} transfer;
} data;
// Required for EventPool - no cleanup needed for POD types
void release() {}
};
// callback function type.
enum ClientState {
@@ -84,6 +119,11 @@ class USBClient : public Component {
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {});
// Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
protected:
bool register_();
TransferRequest *get_trq_();
@@ -91,6 +131,12 @@ class USBClient : public Component {
virtual void on_connected() {}
virtual void on_disconnected() { this->init_pool(); }
// USB task management
static void usb_task_fn(void *arg);
void usb_task_loop();
TaskHandle_t usb_task_handle_{nullptr};
usb_host_client_handle_t handle_{};
usb_device_handle_t device_handle_{};
int device_addr_{-1};

View File

@@ -139,24 +139,40 @@ static std::string get_descriptor_string(const usb_str_desc_t *desc) {
return {buffer};
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr);
// Allocate event from pool
UsbEvent *event = client->event_pool.allocate();
if (event == nullptr) {
// No events available - increment counter for periodic logging
client->event_queue.increment_dropped_count();
return;
}
// Queue events to be processed in main loop
switch (event_msg->event) {
case USB_HOST_CLIENT_EVENT_NEW_DEV: {
auto addr = event_msg->new_dev.address;
ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
client->on_opened(addr);
event->type = EVENT_DEVICE_NEW;
event->data.device_new.address = event_msg->new_dev.address;
break;
}
case USB_HOST_CLIENT_EVENT_DEV_GONE: {
client->on_removed(event_msg->dev_gone.dev_hdl);
ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address);
ESP_LOGD(TAG, "Device gone");
event->type = EVENT_DEVICE_GONE;
event->data.device_gone.handle = event_msg->dev_gone.dev_hdl;
break;
}
default:
ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
break;
client->event_pool.release(event);
return;
}
// Push to lock-free queue (always succeeds since pool size == queue size)
client->event_queue.push(event);
}
void USBClient::setup() {
usb_host_client_config_t config{.is_synchronous = false,
@@ -173,9 +189,59 @@ void USBClient::setup() {
usb_host_transfer_alloc(64, 0, &trq->transfer);
trq->client = this;
}
// Create and start USB task
xTaskCreate(usb_task_fn, "usb_task",
USB_TASK_STACK_SIZE, // Stack size
this, // Task parameter
USB_TASK_PRIORITY, // Priority (higher than main loop)
&this->usb_task_handle_);
if (this->usb_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create USB task");
this->mark_failed();
}
}
void USBClient::usb_task_fn(void *arg) {
auto *client = static_cast<USBClient *>(arg);
client->usb_task_loop();
}
void USBClient::usb_task_loop() {
while (true) {
usb_host_client_handle_events(this->handle_, portMAX_DELAY);
}
}
void USBClient::loop() {
// Process any events from the USB task
UsbEvent *event;
while ((event = this->event_queue.pop()) != nullptr) {
switch (event->type) {
case EVENT_DEVICE_NEW:
this->on_opened(event->data.device_new.address);
break;
case EVENT_DEVICE_GONE:
this->on_removed(event->data.device_gone.handle);
break;
case EVENT_TRANSFER_COMPLETE:
case EVENT_CONTROL_COMPLETE: {
auto *trq = event->data.transfer.trq;
this->release_trq(trq);
break;
}
}
// Return event to pool for reuse
this->event_pool.release(event);
}
// Log dropped events periodically
uint16_t dropped = this->event_queue.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
}
switch (this->state_) {
case USB_CLIENT_OPEN: {
int err;
@@ -228,7 +294,6 @@ void USBClient::loop() {
}
default:
usb_host_client_handle_events(this->handle_, 0);
break;
}
}
@@ -245,6 +310,26 @@ void USBClient::on_removed(usb_device_handle_t handle) {
}
}
// Helper to queue transfer cleanup to main loop
static void queue_transfer_cleanup(TransferRequest *trq, EventType type) {
auto *client = trq->client;
// Allocate event from pool
UsbEvent *event = client->event_pool.allocate();
if (event == nullptr) {
// No events available - increment counter for periodic logging
client->event_queue.increment_dropped_count();
return;
}
event->type = type;
event->data.transfer.trq = trq;
// Push to lock-free queue (always succeeds since pool size == queue size)
client->event_queue.push(event);
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void control_callback(const usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context);
trq->status.error_code = xfer->status;
@@ -252,9 +337,14 @@ static void control_callback(const usb_transfer_t *xfer) {
trq->status.endpoint = xfer->bEndpointAddress;
trq->status.data = xfer->data_buffer;
trq->status.data_len = xfer->actual_num_bytes;
if (trq->callback != nullptr)
// Execute callback in USB task context
if (trq->callback != nullptr) {
trq->callback(trq->status);
trq->client->release_trq(trq);
}
// Queue cleanup to main loop
queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
}
TransferRequest *USBClient::get_trq_() {
@@ -315,6 +405,7 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value,
return true;
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void transfer_callback(usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context);
trq->status.error_code = xfer->status;
@@ -322,9 +413,15 @@ static void transfer_callback(usb_transfer_t *xfer) {
trq->status.endpoint = xfer->bEndpointAddress;
trq->status.data = xfer->data_buffer;
trq->status.data_len = xfer->actual_num_bytes;
if (trq->callback != nullptr)
// Always execute callback in USB task context
// Callbacks should be fast and non-blocking (e.g., copy data to queue)
if (trq->callback != nullptr) {
trq->callback(trq->status);
trq->client->release_trq(trq);
}
// Queue cleanup to main loop
queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
}
/**
* Performs a transfer input operation.

View File

@@ -16,12 +16,12 @@ using namespace bytebuffer;
void USBUartTypeCH34X::enable_channels() {
// enable the channels
for (auto channel : this->channels_) {
if (!channel->initialised_)
if (!channel->initialised_.load())
continue;
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
channel->initialised_ = false;
channel->initialised_.store(false);
}
};
@@ -48,7 +48,7 @@ void USBUartTypeCH34X::enable_channels() {
auto factor = static_cast<uint8_t>(clk / baud_rate);
if (factor == 0 || factor == 0xFF) {
ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate);
channel->initialised_ = false;
channel->initialised_.store(false);
continue;
}
if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1)))

View File

@@ -100,12 +100,12 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
void USBUartTypeCP210X::enable_channels() {
// enable the channels
for (auto channel : this->channels_) {
if (!channel->initialised_)
if (!channel->initialised_.load())
continue;
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
channel->initialised_ = false;
channel->initialised_.store(false);
}
};
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback);

View File

@@ -130,7 +130,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) {
return len;
}
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
if (!this->initialised_) {
if (!this->initialised_.load()) {
ESP_LOGV(TAG, "Channel not initialised - write ignored");
return;
}
@@ -152,7 +152,7 @@ bool USBUartChannel::peek_byte(uint8_t *data) {
return true;
}
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
if (!this->initialised_) {
if (!this->initialised_.load()) {
ESP_LOGV(TAG, "Channel not initialised - read ignored");
return false;
}
@@ -170,7 +170,34 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
return status;
}
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() { USBClient::loop(); }
void USBUartComponent::loop() {
USBClient::loop();
// Process USB data from the lock-free queue
UsbDataChunk *chunk;
while ((chunk = this->usb_data_queue_.pop()) != nullptr) {
auto *channel = chunk->channel;
#ifdef USE_UART_DEBUGGER
if (channel->debug_) {
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector<uint8_t>(chunk->data, chunk->data + chunk->length),
','); // NOLINT()
}
#endif
// Push data to ring buffer (now safe in main loop)
channel->input_buffer_.push(chunk->data, chunk->length);
// Return chunk to pool for reuse
this->chunk_pool_.release(chunk);
}
// Log dropped USB data periodically
uint16_t dropped = this->usb_data_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped);
}
}
void USBUartComponent::dump_config() {
USBClient::dump_config();
for (auto &channel : this->channels_) {
@@ -187,49 +214,70 @@ void USBUartComponent::dump_config() {
}
}
void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_ || channel->input_started_ ||
channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
if (!channel->initialised_.load() || channel->input_started_.load())
return;
// Note: This function is called from both USB task and main loop, so we cannot
// directly check ring buffer space here. Backpressure is handled by the chunk pool:
// when exhausted, USB input stops until chunks are freed by the main loop
const auto *ep = channel->cdc_dev_.in_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// On failure, don't restart - let next read_array() trigger it
channel->input_started_.store(false);
return;
}
#ifdef USE_UART_DEBUGGER
if (channel->debug_) {
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
std::vector<uint8_t>(status.data, status.data + status.data_len), ','); // NOLINT()
}
#endif
channel->input_started_ = false;
if (!channel->dummy_receiver_) {
for (size_t i = 0; i != status.data_len; i++) {
channel->input_buffer_.push(status.data[i]);
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full or we're out of memory
this->usb_data_queue_.increment_dropped_count();
// Mark input as not started so we can retry
channel->input_started_.store(false);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
this->defer([this, channel] { this->start_input(channel); });
}
// On success, restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_started_.store(false);
this->start_input(channel);
};
channel->input_started_ = true;
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
}
void USBUartComponent::start_output(USBUartChannel *channel) {
if (channel->output_started_)
// IMPORTANT: This function must only be called from the main loop!
// The output_buffer_ is not thread-safe and can only be accessed from main loop.
// USB callbacks use defer() to ensure this function runs in the correct context.
if (channel->output_started_.load())
return;
if (channel->output_buffer_.is_empty()) {
return;
}
const auto *ep = channel->cdc_dev_.out_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
channel->output_started_ = false;
channel->output_started_.store(false);
// Defer restart to main loop (defer is thread-safe)
this->defer([this, channel] { this->start_output(channel); });
};
channel->output_started_ = true;
channel->output_started_.store(true);
uint8_t data[ep->wMaxPacketSize];
auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
this->transfer_out(ep->bEndpointAddress, callback, data, len);
@@ -272,7 +320,7 @@ void USBUartTypeCdcAcm::on_connected() {
channel->cdc_dev_ = cdc_devs[i++];
fix_mps(channel->cdc_dev_.in_ep);
fix_mps(channel->cdc_dev_.out_ep);
channel->initialised_ = true;
channel->initialised_.store(true);
auto err =
usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0);
if (err != ESP_OK) {
@@ -301,9 +349,9 @@ void USBUartTypeCdcAcm::on_disconnected() {
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
}
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
channel->initialised_ = false;
channel->input_started_ = false;
channel->output_started_ = false;
channel->initialised_.store(false);
channel->input_started_.store(false);
channel->output_started_.store(false);
channel->input_buffer_.clear();
channel->output_buffer_.clear();
}
@@ -312,10 +360,10 @@ void USBUartTypeCdcAcm::on_disconnected() {
void USBUartTypeCdcAcm::enable_channels() {
for (auto *channel : this->channels_) {
if (!channel->initialised_)
if (!channel->initialised_.load())
continue;
channel->input_started_ = false;
channel->output_started_ = false;
channel->input_started_.store(false);
channel->output_started_.store(false);
this->start_input(channel);
}
}

View File

@@ -5,11 +5,15 @@
#include "esphome/core/helpers.h"
#include "esphome/components/uart/uart_component.h"
#include "esphome/components/usb_host/usb_host.h"
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include <atomic>
namespace esphome {
namespace usb_uart {
class USBUartTypeCdcAcm;
class USBUartComponent;
class USBUartChannel;
static const char *const TAG = "usb_uart";
@@ -68,6 +72,17 @@ class RingBuffer {
uint8_t *buffer_;
};
// Structure for queuing received USB data chunks
struct UsbDataChunk {
static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size
uint8_t data[MAX_CHUNK_SIZE];
uint8_t length; // Max 64 bytes, so uint8_t is sufficient
USBUartChannel *channel;
// Required for EventPool - no cleanup needed for POD types
void release() {}
};
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
friend class USBUartComponent;
friend class USBUartTypeCdcAcm;
@@ -90,16 +105,20 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; }
protected:
const uint8_t index_;
// Larger structures first for better alignment
RingBuffer input_buffer_;
RingBuffer output_buffer_;
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
bool input_started_{true};
bool output_started_{true};
CdcEps cdc_dev_{};
// Enum (likely 4 bytes)
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
// Group atomics together (each 1 byte)
std::atomic<bool> input_started_{true};
std::atomic<bool> output_started_{true};
std::atomic<bool> initialised_{false};
// Group regular bytes together to minimize padding
const uint8_t index_;
bool debug_{};
bool dummy_receiver_{};
bool initialised_{};
};
class USBUartComponent : public usb_host::USBClient {
@@ -115,6 +134,11 @@ class USBUartComponent : public usb_host::USBClient {
void start_input(USBUartChannel *channel);
void start_output(USBUartChannel *channel);
// Lock-free data transfer from USB task to main loop
static constexpr int USB_DATA_QUEUE_SIZE = 32;
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
protected:
std::vector<USBUartChannel *> channels_{};
};

View File

@@ -40,5 +40,7 @@ async def to_code(config):
cg.add_library("Update", None)
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"])
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")

View File

@@ -125,8 +125,8 @@ EAP_AUTH_SCHEMA = cv.All(
cv.Optional(CONF_USERNAME): cv.string_strict,
cv.Optional(CONF_PASSWORD): cv.string_strict,
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All(
cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All(
cv.enum(TTLS_PHASE_2), cv.only_on_esp32
),
cv.Inclusive(
CONF_CERTIFICATE, "certificate_and_key"
@@ -280,11 +280,11 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
cv.decibel, cv.float_range(min=8.5, max=20.5)
),
cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All(
cv.boolean, cv.only_with_esp_idf
cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All(
cv.boolean, cv.only_on_esp32
),
cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All(
cv.boolean, cv.only_with_esp_idf
cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All(
cv.boolean, cv.only_on_esp32
),
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
cv.Optional("enable_mdns"): cv.invalid(
@@ -416,10 +416,10 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040:
elif CORE.is_rp2040:
cg.add_library("WiFi", None)
if CORE.is_esp32 and CORE.using_esp_idf:
if CORE.is_esp32:
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
cg.add_define("USE_WIFI_11KV_SUPPORT")
@@ -506,8 +506,10 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"wifi_component_esp_idf.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"wifi_component_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,

View File

@@ -3,7 +3,7 @@
#include <cinttypes>
#include <map>
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
#include <esp_eap_client.h>
#else
@@ -11,7 +11,7 @@
#endif
#endif
#if defined(USE_ESP32) || defined(USE_ESP_IDF)
#if defined(USE_ESP32)
#include <esp_wifi.h>
#endif
#ifdef USE_ESP8266
@@ -344,7 +344,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},

View File

@@ -20,7 +20,7 @@
#include <WiFi.h>
#endif
#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP)
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#include <esp_eap_client.h>
#else
@@ -113,7 +113,7 @@ struct EAPAuth {
const char *client_cert;
const char *client_key;
// used for EAP-TTLS
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
esp_eap_ttls_phase2_types ttls_phase_2;
#endif
};
@@ -199,7 +199,7 @@ enum WiFiPowerSaveMode : uint8_t {
WIFI_POWER_SAVE_HIGH,
};
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
struct IDFWiFiEvent;
#endif
@@ -368,7 +368,7 @@ class WiFiComponent : public Component {
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
void wifi_scan_done_callback_();
#endif
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
void wifi_process_event_(IDFWiFiEvent *data);
#endif

View File

@@ -1,860 +0,0 @@
#include "wifi_component.h"
#ifdef USE_WIFI
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp_netif.h>
#include <esp_wifi.h>
#include <algorithm>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#include <esp_eap_client.h>
#endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
#include "lwip/apps/sntp.h"
#include "lwip/dns.h"
#include "lwip/err.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
static const char *const TAG = "wifi_esp32";
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_WIFI_AP
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif // USE_WIFI_AP
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WiFiComponent::wifi_pre_setup_() {
uint8_t mac[6];
if (has_custom_mac_address()) {
get_mac_address_raw(mac);
set_mac_address(mac);
}
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
WiFi.onEvent(f);
WiFi.persistent(false);
// Make sure WiFi is in clean state before anything starts
this->wifi_mode_(false, false);
}
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
wifi_mode_t current_mode = WiFiClass::getMode();
bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA;
bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA;
bool set_sta = sta.value_or(current_sta);
bool set_ap = ap.value_or(current_ap);
wifi_mode_t set_mode;
if (set_sta && set_ap) {
set_mode = WIFI_MODE_APSTA;
} else if (set_sta && !set_ap) {
set_mode = WIFI_MODE_STA;
} else if (!set_sta && set_ap) {
set_mode = WIFI_MODE_AP;
} else {
set_mode = WIFI_MODE_NULL;
}
if (current_mode == set_mode)
return true;
if (set_sta && !current_sta) {
ESP_LOGV(TAG, "Enabling STA");
} else if (!set_sta && current_sta) {
ESP_LOGV(TAG, "Disabling STA");
}
if (set_ap && !current_ap) {
ESP_LOGV(TAG, "Enabling AP");
} else if (!set_ap && current_ap) {
ESP_LOGV(TAG, "Disabling AP");
}
bool ret = WiFiClass::mode(set_mode);
if (!ret) {
ESP_LOGW(TAG, "Setting mode failed");
return false;
}
// WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and
// esp_netif_create_default_wifi_ap(), which creates the interfaces.
// s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event
#ifdef USE_WIFI_AP
if (set_ap)
s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
#endif
return ret;
}
bool WiFiComponent::wifi_sta_pre_setup_() {
if (!this->wifi_mode_(true, {}))
return false;
WiFi.setAutoReconnect(false);
delay(10);
return true;
}
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
int8_t val = static_cast<int8_t>(output_power * 4);
return esp_wifi_set_max_tx_power(val) == ESP_OK;
}
bool WiFiComponent::wifi_apply_power_save_() {
wifi_ps_type_t power_save;
switch (this->power_save_) {
case WIFI_POWER_SAVE_LIGHT:
power_save = WIFI_PS_MIN_MODEM;
break;
case WIFI_POWER_SAVE_HIGH:
power_save = WIFI_PS_MAX_MODEM;
break;
case WIFI_POWER_SAVE_NONE:
default:
power_save = WIFI_PS_NONE;
break;
}
return esp_wifi_set_ps(power_save) == ESP_OK;
}
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
ESP_LOGE(TAG, "SSID too long");
return false;
}
if (ap.get_password().size() > sizeof(conf.sta.password)) {
ESP_LOGE(TAG, "Password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
// The weakest authmode to accept in the fast scan mode
if (ap.get_password().empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else {
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
}
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
}
#endif
if (ap.get_bssid().has_value()) {
conf.sta.bssid_set = true;
memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
} else {
conf.sta.bssid_set = false;
}
if (ap.get_channel().has_value()) {
conf.sta.channel = *ap.get_channel();
conf.sta.scan_method = WIFI_FAST_SCAN;
} else {
conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
}
// Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set.
// Units: AP beacon intervals. Defaults to 3 if set to 0.
conf.sta.listen_interval = 0;
// Protected Management Frame
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
conf.sta.pmf_cfg.capable = true;
conf.sta.pmf_cfg.required = false;
// note, we do our own filtering
// The minimum rssi to accept in the fast scan mode
conf.sta.threshold.rssi = -127;
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
wifi_config_t current_conf;
esp_err_t err;
err = esp_wifi_get_config(WIFI_IF_STA, &current_conf);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
// can continue
}
if (memcmp(&current_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT
err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err));
return false;
}
}
err = esp_wifi_set_config(WIFI_IF_STA, &conf);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err));
return false;
}
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
// setup enterprise authentication if required
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
EAPAuth eap = ap.get_eap().value();
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err);
}
int ca_cert_len = strlen(eap.ca_cert);
int client_cert_len = strlen(eap.client_cert);
int client_key_len = strlen(eap.client_key);
if (ca_cert_len) {
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err);
}
}
// workout what type of EAP this is
// validation is not required as the config tool has already validated it
if (client_cert_len && client_key_len) {
// if we have certs, this must be EAP-TLS
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err);
}
} else {
// in the absence of certs, assume this is username/password based
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err);
}
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err);
}
}
err = esp_wifi_sta_enterprise_enable();
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err);
}
}
#endif // USE_WIFI_WPA2_EAP
this->wifi_apply_hostname_();
s_sta_connecting = true;
err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// Check if the STA interface is initialized before using it
if (s_sta_netif == nullptr) {
ESP_LOGW(TAG, "STA interface not initialized");
return false;
}
esp_netif_dhcp_status_t dhcp_status;
esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err));
return false;
}
if (!manual_ip.has_value()) {
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526
{
LwIPLock lock;
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
}
// No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
err = esp_netif_dhcpc_start(s_sta_netif);
if (err != ESP_OK) {
ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
}
return err == ESP_OK;
}
return true;
}
esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
info.ip = manual_ip->static_ip;
info.gw = manual_ip->gateway;
info.netmask = manual_ip->subnet;
err = esp_netif_dhcpc_stop(s_sta_netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
}
err = esp_netif_set_ip_info(s_sta_netif, &info);
if (err != ESP_OK) {
ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
}
esp_netif_dns_info_t dns;
if (manual_ip->dns1.is_set()) {
dns.ip = manual_ip->dns1;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
}
if (manual_ip->dns2.is_set()) {
dns.ip = manual_ip->dns2;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
}
return true;
}
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
if (!this->has_sta())
return {};
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
bool WiFiComponent::wifi_apply_hostname_() {
// setting is done in SYSTEM_EVENT_STA_START callback
return true;
}
const char *get_auth_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_AUTH_OPEN:
return "OPEN";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA PSK";
case WIFI_AUTH_WPA2_PSK:
return "WPA2 PSK";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2 PSK";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA2 Enterprise";
case WIFI_AUTH_WPA3_PSK:
return "WPA3 PSK";
case WIFI_AUTH_WPA2_WPA3_PSK:
return "WPA2/WPA3 PSK";
case WIFI_AUTH_WAPI_PSK:
return "WAPI PSK";
default:
return "UNKNOWN";
}
}
using esphome_ip4_addr_t = esp_ip4_addr_t;
std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
char buf[20];
sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
uint8_t(ip.addr >> 24));
return buf;
}
const char *get_op_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_OFF:
return "OFF";
case WIFI_STA:
return "STA";
case WIFI_AP:
return "AP";
case WIFI_AP_STA:
return "AP+STA";
default:
return "UNKNOWN";
}
}
const char *get_disconnect_reason_str(uint8_t reason) {
switch (reason) {
case WIFI_REASON_AUTH_EXPIRE:
return "Auth Expired";
case WIFI_REASON_AUTH_LEAVE:
return "Auth Leave";
case WIFI_REASON_ASSOC_EXPIRE:
return "Association Expired";
case WIFI_REASON_ASSOC_TOOMANY:
return "Too Many Associations";
case WIFI_REASON_NOT_AUTHED:
return "Not Authenticated";
case WIFI_REASON_NOT_ASSOCED:
return "Not Associated";
case WIFI_REASON_ASSOC_LEAVE:
return "Association Leave";
case WIFI_REASON_ASSOC_NOT_AUTHED:
return "Association not Authenticated";
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
return "Disassociate Power Cap Bad";
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
return "Disassociate Supported Channel Bad";
case WIFI_REASON_IE_INVALID:
return "IE Invalid";
case WIFI_REASON_MIC_FAILURE:
return "Mic Failure";
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
return "4-Way Handshake Timeout";
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
return "Group Key Update Timeout";
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
return "IE In 4-Way Handshake Differs";
case WIFI_REASON_GROUP_CIPHER_INVALID:
return "Group Cipher Invalid";
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
return "Pairwise Cipher Invalid";
case WIFI_REASON_AKMP_INVALID:
return "AKMP Invalid";
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
return "Unsupported RSN IE version";
case WIFI_REASON_INVALID_RSN_IE_CAP:
return "Invalid RSN IE Cap";
case WIFI_REASON_802_1X_AUTH_FAILED:
return "802.1x Authentication Failed";
case WIFI_REASON_CIPHER_SUITE_REJECTED:
return "Cipher Suite Rejected";
case WIFI_REASON_BEACON_TIMEOUT:
return "Beacon Timeout";
case WIFI_REASON_NO_AP_FOUND:
return "AP Not Found";
case WIFI_REASON_AUTH_FAIL:
return "Authentication Failed";
case WIFI_REASON_ASSOC_FAIL:
return "Association Failed";
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return "Handshake Failed";
case WIFI_REASON_CONNECTION_FAIL:
return "Connection Failed";
case WIFI_REASON_AP_TSF_RESET:
return "AP TSF reset";
case WIFI_REASON_ROAMING:
return "Station Roaming";
case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG:
return "Association comeback time too long";
case WIFI_REASON_SA_QUERY_TIMEOUT:
return "SA query timeout";
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
return "No AP found with compatible security";
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
return "No AP found in auth mode threshold";
case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD:
return "No AP found in RSSI threshold";
case WIFI_REASON_UNSPECIFIED:
default:
return "Unspecified";
}
}
void WiFiComponent::wifi_loop_() {}
#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
using esphome_wifi_event_id_t = arduino_event_id_t;
using esphome_wifi_event_info_t = arduino_event_info_t;
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
switch (event) {
case ESPHOME_EVENT_ID_WIFI_READY: {
ESP_LOGV(TAG, "Ready");
break;
}
case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
auto it = info.wifi_scan_done;
ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
this->wifi_scan_done_callback_();
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_START: {
ESP_LOGV(TAG, "STA start");
// apply hostname
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
ESP_LOGV(TAG, "STA stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
auto it = info.wifi_sta_connected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
#if USE_NETWORK_IPV6
this->set_timeout(100, [] { WiFi.enableIPv6(); });
#endif /* USE_NETWORK_IPV6 */
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
auto it = info.wifi_sta_disconnected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
err_t err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err));
}
this->error_from_callback_ = true;
}
s_sta_connecting = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
auto it = info.wifi_sta_authmode_change;
ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
// Mitigate CVE-2020-12638
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
// we can't call retry_connect() from this context, so disconnect immediately
// and notify main thread with error_from_callback_
err_t err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
}
this->error_from_callback_ = true;
}
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
auto it = info.got_ip.ip_info;
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str());
this->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6
s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT;
#else
s_sta_connecting = false;
#endif /* USE_NETWORK_IPV6 */
break;
}
#if USE_NETWORK_IPV6
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
auto it = info.got_ip6.ip6_info;
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip));
this->num_ipv6_addresses_++;
s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT));
break;
}
#endif /* USE_NETWORK_IPV6 */
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
ESP_LOGV(TAG, "Lost IP");
this->got_ipv4_address_ = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_START: {
ESP_LOGV(TAG, "AP start");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
ESP_LOGV(TAG, "AP stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
auto it = info.wifi_sta_connected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
auto it = info.wifi_sta_disconnected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
ESP_LOGV(TAG, "AP client assigned IP");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
auto it = info.wifi_ap_probereqrecved;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
break;
}
default:
break;
}
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
const auto status = WiFi.status();
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
}
if (status == WL_NO_SSID_AVAIL) {
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
}
if (s_sta_connecting) {
return WiFiSTAConnectStatus::CONNECTING;
}
if (status == WL_CONNECTED) {
return WiFiSTAConnectStatus::CONNECTED;
}
return WiFiSTAConnectStatus::IDLE;
}
bool WiFiComponent::wifi_scan_start_(bool passive) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {
ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
return false;
}
return true;
}
void WiFiComponent::wifi_scan_done_callback_() {
this->scan_result_.clear();
int16_t num = WiFi.scanComplete();
if (num < 0)
return;
this->scan_result_.reserve(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
this->scan_result_.push_back(scan);
}
WiFi.scanDelete();
this->scan_done_ = true;
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err;
// enable AP
if (!this->wifi_mode_({}, true))
return false;
// Check if the AP interface is initialized before using it
if (s_ap_netif == nullptr) {
ESP_LOGW(TAG, "AP interface not initialized");
return false;
}
esp_netif_ip_info_t info;
if (manual_ip.has_value()) {
info.ip = manual_ip->static_ip;
info.gw = manual_ip->gateway;
info.netmask = manual_ip->subnet;
} else {
info.ip = network::IPAddress(192, 168, 4, 1);
info.gw = network::IPAddress(192, 168, 4, 1);
info.netmask = network::IPAddress(255, 255, 255, 0);
}
err = esp_netif_dhcps_stop(s_ap_netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err));
return false;
}
err = esp_netif_set_ip_info(s_ap_netif, &info);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
return false;
}
dhcps_lease_t lease;
lease.enable = true;
network::IPAddress start_address = network::IPAddress(&info.ip);
start_address += 99;
lease.start_ip = start_address;
ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str());
start_address += 10;
lease.end_ip = start_address;
ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str());
err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
return false;
}
err = esp_netif_dhcps_start(s_ap_netif);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
return false;
}
return true;
}
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
ESP_LOGE(TAG, "AP SSID too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
conf.ap.channel = ap.get_channel().value_or(1);
conf.ap.ssid_hidden = ap.get_ssid().size();
conf.ap.max_connection = 5;
conf.ap.beacon_interval = 100;
if (ap.get_password().empty()) {
conf.ap.authmode = WIFI_AUTH_OPEN;
*conf.ap.password = 0;
} else {
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
if (ap.get_password().size() > sizeof(conf.ap.password)) {
ESP_LOGE(TAG, "AP password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
}
// pairwise cipher of SoftAP, group cipher will be derived using this.
conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err);
return false;
}
yield();
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(s_ap_netif, &ip);
return network::IPAddress(&ip.ip);
}
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{};
uint8_t *raw_bssid = WiFi.BSSID();
if (raw_bssid != nullptr) {
for (size_t i = 0; i < bssid.size(); i++)
bssid[i] = raw_bssid[i];
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); }
} // namespace wifi
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
#endif

View File

@@ -1,7 +1,7 @@
#include "wifi_component.h"
#ifdef USE_WIFI
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include <esp_event.h>
#include <esp_netif.h>
@@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
} // namespace wifi
} // namespace esphome
#endif // USE_ESP_IDF
#endif // USE_ESP32
#endif

View File

View File

@@ -0,0 +1,41 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
CONF_WTS01_ID = "wts01_id"
CODEOWNERS = ["@alepee"]
DEPENDENCIES = ["uart"]
wts01_ns = cg.esphome_ns.namespace("wts01")
WTS01Sensor = wts01_ns.class_(
"WTS01Sensor", cg.Component, uart.UARTDevice, sensor.Sensor
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
WTS01Sensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"wts01",
baud_rate=9600,
require_rx=True,
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)

View File

@@ -0,0 +1,91 @@
#include "wts01.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome {
namespace wts01 {
constexpr uint8_t HEADER_1 = 0x55;
constexpr uint8_t HEADER_2 = 0x01;
constexpr uint8_t HEADER_3 = 0x01;
constexpr uint8_t HEADER_4 = 0x04;
static const char *const TAG = "wts01";
void WTS01Sensor::loop() {
// Process all available data at once
while (this->available()) {
uint8_t c;
if (this->read_byte(&c)) {
this->handle_char_(c);
}
}
}
void WTS01Sensor::dump_config() { LOG_SENSOR("", "WTS01 Sensor", this); }
void WTS01Sensor::handle_char_(uint8_t c) {
// State machine for processing the header. Reset if something doesn't match.
if (this->buffer_pos_ == 0 && c != HEADER_1) {
return;
}
if (this->buffer_pos_ == 1 && c != HEADER_2) {
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ == 2 && c != HEADER_3) {
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ == 3 && c != HEADER_4) {
this->buffer_pos_ = 0;
return;
}
// Add byte to buffer
this->buffer_[this->buffer_pos_++] = c;
// Process complete packet
if (this->buffer_pos_ >= PACKET_SIZE) {
this->process_packet_();
this->buffer_pos_ = 0;
}
}
void WTS01Sensor::process_packet_() {
// Based on Tasmota implementation
// Format: 55 01 01 04 01 11 16 12 95
// header T Td Ck - T = Temperature, Td = Temperature decimal, Ck = Checksum
uint8_t calculated_checksum = 0;
for (uint8_t i = 0; i < PACKET_SIZE - 1; i++) {
calculated_checksum += this->buffer_[i];
}
uint8_t received_checksum = this->buffer_[PACKET_SIZE - 1];
if (calculated_checksum != received_checksum) {
ESP_LOGW(TAG, "WTS01 Checksum doesn't match: 0x%02X != 0x%02X", received_checksum, calculated_checksum);
return;
}
// Extract temperature value
int8_t temp = this->buffer_[6];
int32_t sign = 1;
// Handle negative temperatures
if (temp < 0) {
sign = -1;
}
// Calculate temperature (temp + decimal/100)
float temperature = static_cast<float>(temp) + (sign * static_cast<float>(this->buffer_[7]) / 100.0f);
ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature);
this->publish_state(temperature);
}
} // namespace wts01
} // namespace esphome

View File

@@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace wts01 {
constexpr uint8_t PACKET_SIZE = 9;
class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Component {
public:
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
uint8_t buffer_[PACKET_SIZE];
uint8_t buffer_pos_{0};
void handle_char_(uint8_t c);
void process_packet_();
};
} // namespace wts01
} // namespace esphome

View File

@@ -1,4 +1,5 @@
#include "zwave_proxy.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -12,6 +13,7 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
// Calculate Z-Wave frame checksum
@@ -26,7 +28,44 @@ static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
void ZWaveProxy::setup() { this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); }
void ZWaveProxy::setup() {
this->setup_time_ = App.get_loop_component_start_time();
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
}
float ZWaveProxy::get_setup_priority() const {
// Set up before API so home ID is ready when API starts
return setup_priority::BEFORE_CONNECTION;
}
bool ZWaveProxy::can_proceed() {
// If we already have the home ID, we can proceed
if (this->home_id_ready_) {
return true;
}
// Handle any pending responses
if (this->response_handler_()) {
ESP_LOGV(TAG, "Handled response during setup");
}
// Process UART data to check for home ID
this->process_uart_();
// Check if we got the home ID after processing
if (this->home_id_ready_) {
return true;
}
// Wait up to HOME_ID_TIMEOUT_MS for home ID response
const uint32_t now = App.get_loop_component_start_time();
if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
ESP_LOGW(TAG, "Timeout reading Home ID during setup");
return true; // Proceed anyway after timeout
}
return false; // Keep waiting
}
void ZWaveProxy::loop() {
if (this->response_handler_()) {
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
this->api_connection_ = nullptr; // Unsubscribe if disconnected
}
this->process_uart_();
this->status_clear_warning();
}
void ZWaveProxy::process_uart_() {
while (this->available()) {
uint8_t byte;
if (!this->read_byte(&byte)) {
@@ -56,24 +100,24 @@ void ZWaveProxy::loop() {
// Extract the 4-byte Home ID starting at offset 4
// The frame parser has already validated the checksum and ensured all bytes are present
std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size());
this->home_id_ready_ = true;
ESP_LOGI(TAG, "Home ID: %s",
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
}
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
if (this->api_connection_ != nullptr) {
// minimize copying to reduce CPU overhead
// Zero-copy: point directly to our buffer
this->outgoing_proto_msg_.data = this->buffer_.data();
if (this->in_bootloader_) {
this->outgoing_proto_msg_.data_len = this->buffer_index_;
} else {
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
}
std::memcpy(this->outgoing_proto_msg_.data, this->buffer_.data(), this->outgoing_proto_msg_.data_len);
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
}
}
}
this->status_clear_warning();
}
void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }
@@ -228,7 +272,9 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
}
// Forward response (ACK/NAK/CAN) back to client for processing
if (this->api_connection_ != nullptr) {
this->outgoing_proto_msg_.data[0] = byte;
// Store single byte in buffer and point to it
this->buffer_[0] = byte;
this->outgoing_proto_msg_.data = this->buffer_.data();
this->outgoing_proto_msg_.data_len = 1;
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
}

View File

@@ -11,6 +11,8 @@
namespace esphome {
namespace zwave_proxy {
static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size
enum ZWaveResponseTypes : uint8_t {
ZWAVE_FRAME_TYPE_ACK = 0x06,
ZWAVE_FRAME_TYPE_CAN = 0x18,
@@ -44,6 +46,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override;
bool can_proceed() override;
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
api::APIConnection *get_api_connection() { return this->api_connection_; }
@@ -60,19 +64,24 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
void parse_start_(uint8_t byte);
bool response_handler_();
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
uint8_t buffer_index_{0}; // Index for populating the data buffer
uint8_t end_frame_after_{0}; // Payload reception ends after this index
uint8_t last_response_{0}; // Last response type sent
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
void process_uart_(); // Process all available UART data
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
// Pointers and 32-bit values (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
uint32_t setup_time_{0}; // Time when setup() was called
// 8-bit values (grouped together to minimize padding)
uint8_t buffer_index_{0}; // Index for populating the data buffer
uint8_t end_frame_after_{0}; // Payload reception ends after this index
uint8_t last_response_{0}; // Last response type sent
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module
};
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -396,7 +396,7 @@ async def add_includes(includes: list[str]) -> None:
async def _add_platformio_options(pio_options):
# Add includes at the very end, so that they override everything
for key, val in pio_options.items():
if key == "build_flags" and not isinstance(val, list):
if key in ["build_flags", "lib_ignore"] and not isinstance(val, list):
val = [val]
cg.add_platformio_option(key, val)

View File

@@ -158,6 +158,7 @@
#define USE_ESP32_BLE_SERVER
#define USE_ESP32_BLE_UUID
#define USE_ESP32_BLE_ADVERTISING
#define USE_ESP32_CAMERA_JPEG_ENCODER
#define USE_I2C
#define USE_IMPROV
#define USE_MICROPHONE

View File

@@ -39,7 +39,7 @@ class HashBase {
/// Compare the hash against a provided hex-encoded hash
bool equals_hex(const char *expected) {
uint8_t parsed[32]; // Max size for SHA256
uint8_t parsed[this->get_size()];
if (!parse_hex(expected, parsed, this->get_size())) {
return false;
}
@@ -50,7 +50,7 @@ class HashBase {
virtual size_t get_size() const = 0;
protected:
uint8_t digest_[32]; // Common digest storage, sized for largest hash (SHA256)
uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes
};
} // namespace esphome

View File

@@ -1,5 +1,5 @@
import abc
from collections.abc import Callable, Sequence
from collections.abc import Callable
import inspect
import math
import re
@@ -13,7 +13,6 @@ from esphome.core import (
HexInt,
Lambda,
Library,
TimePeriod,
TimePeriodMicroseconds,
TimePeriodMilliseconds,
TimePeriodMinutes,
@@ -21,35 +20,11 @@ from esphome.core import (
TimePeriodSeconds,
)
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
from esphome.types import Expression, SafeExpType, TemplateArgsType
from esphome.util import OrderedDict
from esphome.yaml_util import ESPHomeDataBase
class Expression(abc.ABC):
__slots__ = ()
@abc.abstractmethod
def __str__(self):
"""
Convert expression into C++ code
"""
SafeExpType = (
Expression
| bool
| str
| str
| int
| float
| TimePeriod
| type[bool]
| type[int]
| type[float]
| Sequence[Any]
)
class RawExpression(Expression):
__slots__ = ("text",)
@@ -575,7 +550,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
return obj
def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
"""Declare a new pointer variable in the code generation by calling it's constructor
with the given arguments.
@@ -681,7 +656,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda(
value: Lambda,
parameters: list[tuple[SafeExpType, str]],
parameters: TemplateArgsType,
capture: str = "=",
return_type: SafeExpType = None,
) -> LambdaExpression | None:

View File

@@ -283,11 +283,23 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
def _stdout_thread(self) -> None:
if not self._use_popen:
return
line = b""
cr = False
while True:
data = self._proc.stdout.readline()
data = self._proc.stdout.read(1)
if data:
data = data.replace(b"\r", b"")
self._queue.put_nowait(data)
if data == b"\r":
cr = True
elif data == b"\n":
self._queue.put_nowait(line + b"\n")
line = b""
cr = False
elif cr:
self._queue.put_nowait(line + b"\r")
line = data
cr = False
else:
line += data
if self._proc.poll() is not None:
break
self._proc.wait(1.0)
@@ -479,6 +491,14 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
class EsphomeCleanAllHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
clean_build_dir = json_message.get("clean_build_dir", True)
if clean_build_dir:
return [*DASHBOARD_COMMAND, "clean-all", settings.config_dir]
return [*DASHBOARD_COMMAND, "clean-all"]
class EsphomeCleanHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"])
@@ -1313,6 +1333,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
(f"{rel}compile", EsphomeCompileHandler),
(f"{rel}validate", EsphomeValidateHandler),
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
(f"{rel}clean-all", EsphomeCleanAllHandler),
(f"{rel}clean", EsphomeCleanHandler),
(f"{rel}vscode", EsphomeVscodeHandler),
(f"{rel}ace", EsphomeAceEditorHandler),

View File

@@ -1,8 +1,10 @@
"""This helper module tracks commonly used types in the esphome python codebase."""
from typing import TypedDict
import abc
from collections.abc import Sequence
from typing import Any, TypedDict
from esphome.core import ID, EsphomeCore, Lambda
from esphome.core import ID, EsphomeCore, Lambda, TimePeriod
ConfigFragmentType = (
str
@@ -20,6 +22,32 @@ CoreType = EsphomeCore
ConfigPathType = str | int
class Expression(abc.ABC):
__slots__ = ()
@abc.abstractmethod
def __str__(self):
"""
Convert expression into C++ code
"""
SafeExpType = (
Expression
| bool
| str
| int
| float
| TimePeriod
| type[bool]
| type[int]
| type[float]
| Sequence[Any]
)
TemplateArgsType = list[tuple[SafeExpType, str]]
class EntityMetadata(TypedDict):
"""Metadata stored for each entity to help with duplicate detection."""

View File

@@ -1,19 +1,30 @@
import collections
from collections.abc import Callable
import io
import logging
from pathlib import Path
import re
import subprocess
import sys
from typing import Any
from typing import TYPE_CHECKING, Any
from esphome import const
_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from esphome.config_validation import Schema
from esphome.cpp_generator import MockObjClass
class RegistryEntry:
def __init__(self, name, fun, type_id, schema):
def __init__(
self,
name: str,
fun: Callable[..., Any],
type_id: "MockObjClass",
schema: "Schema",
):
self.name = name
self.fun = fun
self.type_id = type_id
@@ -38,8 +49,8 @@ class Registry(dict[str, RegistryEntry]):
self.base_schema = base_schema or {}
self.type_id_key = type_id_key
def register(self, name, type_id, schema):
def decorator(fun):
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
def decorator(fun: Callable[..., Any]):
self[name] = RegistryEntry(name, fun, type_id, schema)
return fun
@@ -47,8 +58,8 @@ class Registry(dict[str, RegistryEntry]):
class SimpleRegistry(dict):
def register(self, name, data):
def decorator(fun):
def register(self, name: str, data: Any):
def decorator(fun: Callable[..., Any]):
self[name] = (fun, data)
return fun

View File

@@ -323,17 +323,41 @@ def clean_build():
# Clean PlatformIO cache to resolve CMake compiler detection issues
# This helps when toolchain paths change or get corrupted
try:
from platformio.project.helpers import get_project_cache_dir
from platformio.project.config import ProjectConfig
except ImportError:
# PlatformIO is not available, skip cache cleaning
pass
else:
cache_dir = get_project_cache_dir()
if cache_dir and cache_dir.strip():
cache_path = Path(cache_dir)
if cache_path.is_dir():
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
shutil.rmtree(cache_dir)
config = ProjectConfig.get_instance()
cache_dir = Path(config.get("platformio", "cache_dir"))
if cache_dir.is_dir():
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
shutil.rmtree(cache_dir)
def clean_all(configuration: list[str]):
import shutil
# Clean entire build dir
for dir in configuration:
buid_dir = Path(dir) / ".esphome"
if buid_dir.is_dir():
_LOGGER.info("Deleting %s", buid_dir)
shutil.rmtree(buid_dir)
# Clean PlatformIO project files
try:
from platformio.project.config import ProjectConfig
except ImportError:
# PlatformIO is not available, skip cleaning
pass
else:
config = ProjectConfig.get_instance()
for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]:
path = Path(config.get("platformio", pio_dir))
if path.is_dir():
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
shutil.rmtree(path)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome

View File

@@ -12,10 +12,11 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20250904.0
aioesphomeapi==41.4.0
aioesphomeapi==41.10.0
zeroconf==0.147.2
puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import
ruamel.yaml.clib==0.2.12 # dashboard_import
esphome-glyphsets==0.2.0
pillow==10.4.0
cairosvg==2.8.2

View File

@@ -1,6 +1,6 @@
pylint==3.3.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.13.1 # also change in .pre-commit-config.yaml when updating
ruff==0.13.2 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -353,12 +353,33 @@ def create_field_type_info(
return FixedArrayRepeatedType(field, size_define)
return RepeatedTypeInfo(field)
# Check for fixed_array_size option on bytes fields
if (
field.type == 12
and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None
):
return FixedArrayBytesType(field, fixed_size)
# Check for mutually exclusive options on bytes fields
if field.type == 12:
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
fixed_size = get_field_opt(field, pb.fixed_array_size, None)
if has_pointer_to_buffer and fixed_size is not None:
raise ValueError(
f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. "
"These options are mutually exclusive. Use pointer_to_buffer for zero-copy "
"or fixed_array_size for traditional array storage."
)
if has_pointer_to_buffer:
# Zero-copy pointer approach - no size needed, will use size_t for length
return PointerToBytesBufferType(field, None)
if fixed_size is not None:
# Traditional fixed array approach with copy
return FixedArrayBytesType(field, fixed_size)
# Check for pointer_to_buffer option on string fields
if field.type == 9:
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
if has_pointer_to_buffer:
# Zero-copy pointer approach for strings
return PointerToBytesBufferType(field, None)
# Special handling for bytes fields
if field.type == 12:
@@ -818,6 +839,91 @@ class BytesType(TypeInfo):
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
class PointerToBytesBufferType(TypeInfo):
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
@classmethod
def can_use_dump_field(cls) -> bool:
return False
def __init__(
self, field: descriptor.FieldDescriptorProto, size: int | None = None
) -> None:
super().__init__(field)
# Size is not used for pointer_to_buffer - we always use size_t for length
self.array_size = 0
@property
def cpp_type(self) -> str:
return "const uint8_t*"
@property
def default_value(self) -> str:
return "nullptr"
@property
def reference_type(self) -> str:
return "const uint8_t*"
@property
def const_reference_type(self) -> str:
return "const uint8_t*"
@property
def public_content(self) -> list[str]:
# Use uint16_t for length - max packet size is well below 65535
# Add pointer and length fields
return [
f"const uint8_t* {self.field_name}{{nullptr}};",
f"uint16_t {self.field_name}_len{{0}};",
]
@property
def encode_content(self) -> str:
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
@property
def decode_length_content(self) -> str | None:
# Decode directly stores the pointer to avoid allocation
return f"""case {self.number}: {{
// Use raw data directly to avoid allocation
this->{self.field_name} = value.data();
this->{self.field_name}_len = value.size();
break;
}}"""
@property
def decode_length(self) -> str | None:
# This is handled in decode_length_content
return None
@property
def wire_type(self) -> WireType:
"""Get the wire type for this bytes field."""
return WireType.LENGTH_DELIMITED # Uses wire type 2
def dump(self, name: str) -> str:
return (
f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)"
)
@property
def dump_content(self) -> str:
# Custom dump that doesn't use dump_field template
return (
f'out.append(" {self.name}: ");\n'
+ f"out.append({self.dump(self.field_name)});\n"
+ 'out.append("\\n");'
)
def get_size_calculation(self, name: str, force: bool = False) -> str:
return f"size.add_length({self.number}, this->{self.field_name}_len);"
def get_estimated_size(self) -> int:
# field ID + length varint + typical data (assume small for pointer fields)
return self.calculate_field_id_size() + 2 + 16
class FixedArrayBytesType(TypeInfo):
"""Special type for fixed-size byte arrays."""
@@ -2615,6 +2721,10 @@ static const char *const TAG = "api.service";
hpp_protected = ""
cpp += "\n"
# Build a mapping of message input types to their authentication requirements
message_auth_map: dict[str, bool] = {}
message_conn_map: dict[str, bool] = {}
m = serv.method[0]
for m in serv.method:
func = m.name
@@ -2626,6 +2736,10 @@ static const char *const TAG = "api.service";
needs_conn = get_opt(m, pb.needs_setup_connection, True)
needs_auth = get_opt(m, pb.needs_authentication, True)
# Store authentication requirements for message types
message_auth_map[inp] = needs_auth
message_conn_map[inp] = needs_conn
ifdef = message_ifdef_map.get(inp, ifdefs.get(inp))
if ifdef is not None:
@@ -2643,33 +2757,14 @@ static const char *const TAG = "api.service";
cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
# Start with authentication/connection check if needed
if needs_auth or needs_conn:
# Determine which check to use
if needs_auth:
check_func = "this->check_authenticated_()"
else:
check_func = "this->check_connection_setup_()"
if is_void:
# For void methods, just wrap with auth check
body = f"if ({check_func}) {{\n"
body += f" this->{func}(msg);\n"
body += "}\n"
else:
# For non-void methods, combine auth check and send response check
body = f"if ({check_func} && !this->send_{func}_response(msg)) {{\n"
body += " this->on_fatal_error();\n"
body += "}\n"
# No authentication check here - it's done in read_message
body = ""
if is_void:
body += f"this->{func}(msg);\n"
else:
# No auth check needed, just call the handler
body = ""
if is_void:
body += f"this->{func}(msg);\n"
else:
body += f"if (!this->send_{func}_response(msg)) {{\n"
body += " this->on_fatal_error();\n"
body += "}\n"
body += f"if (!this->send_{func}_response(msg)) {{\n"
body += " this->on_fatal_error();\n"
body += "}\n"
cpp += indent(body) + "\n" + "}\n"
@@ -2678,6 +2773,65 @@ static const char *const TAG = "api.service";
hpp_protected += "#endif\n"
cpp += "#endif\n"
# Generate optimized read_message with authentication checking
# Categorize messages by their authentication requirements
no_conn_ids: set[int] = set()
conn_only_ids: set[int] = set()
for id_, (_, _, case_msg_name) in cases:
if case_msg_name in message_auth_map:
needs_auth = message_auth_map[case_msg_name]
needs_conn = message_conn_map[case_msg_name]
if not needs_conn:
no_conn_ids.add(id_)
elif not needs_auth:
conn_only_ids.add(id_)
# Generate override if we have messages that skip checks
if no_conn_ids or conn_only_ids:
# Helper to generate case statements with ifdefs
def generate_cases(ids: set[int], comment: str) -> str:
result = ""
for id_ in sorted(ids):
_, ifdef, msg_name = RECEIVE_CASES[id_]
if ifdef:
result += f"#ifdef {ifdef}\n"
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n"
if ifdef:
result += "#endif\n"
return result
hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
cpp += " // Check authentication/connection requirements for messages\n"
cpp += " switch (msg_type) {\n"
# Messages that don't need any checks
if no_conn_ids:
cpp += generate_cases(no_conn_ids, "// No setup required")
cpp += " break; // Skip all checks for these messages\n"
# Messages that only need connection setup
if conn_only_ids:
cpp += generate_cases(conn_only_ids, "// Connection setup only")
cpp += " if (!this->check_connection_setup_()) {\n"
cpp += " return; // Connection not setup\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " default:\n"
cpp += " // All other messages require authentication (which includes connection check)\n"
cpp += " if (!this->check_authenticated_()) {\n"
cpp += " return; // Authentication failed\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " }\n\n"
cpp += " // Call base implementation to process the message\n"
cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n"
cpp += "}\n"
hpp += " protected:\n"
hpp += hpp_protected
hpp += "};\n"

View File

@@ -13,7 +13,6 @@ esphome:
api:
port: 8000
password: pwd
reboot_timeout: 0min
encryption:
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=

View File

@@ -0,0 +1,7 @@
uart:
rx_pin: ${rx_pin}
baud_rate: 9600
sensor:
- platform: wts01
id: wts01_sensor

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO16
rx_pin: GPIO17
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO6
rx_pin: GPIO7
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO6
rx_pin: GPIO7
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO16
rx_pin: GPIO17
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO1
rx_pin: GPIO3
<<: !include common.yaml

View File

@@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO1
<<: !include common.yaml

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from aioesphomeapi import APIConnectionError
from aioesphomeapi import APIConnectionError, InvalidAuthAPIError
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -48,6 +48,22 @@ async def test_host_mode_api_password(
assert len(states) > 0
# Test with wrong password - should fail
with pytest.raises(APIConnectionError, match="Invalid password"):
async with api_client_connected(password="wrong_password"):
pass # Should not reach here
# Try connecting with wrong password
try:
async with api_client_connected(
password="wrong_password", timeout=5
) as client:
# If we get here without exception, try to use the connection
# which should fail if auth failed
await client.device_info_and_list_entities()
# If we successfully got device info and entities, auth didn't fail properly
pytest.fail("Connection succeeded with wrong password")
except (InvalidAuthAPIError, APIConnectionError) as e:
# Expected - auth should fail
# Accept either InvalidAuthAPIError or generic APIConnectionError
# since the client might not always distinguish
assert (
"password" in str(e).lower()
or "auth" in str(e).lower()
or "invalid" in str(e).lower()
)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Generator
from dataclasses import dataclass
import logging
from pathlib import Path
import re
from typing import Any
@@ -16,6 +17,7 @@ from esphome import platformio_api
from esphome.__main__ import (
Purpose,
choose_upload_log_host,
command_clean_all,
command_rename,
command_update_all,
command_wizard,
@@ -1853,3 +1855,95 @@ esp32:
# Should not have any Python error messages
assert "TypeError" not in clean_output
assert "can only concatenate str" not in clean_output
def test_command_clean_all_success(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_all when writer.clean_all() succeeds."""
args = MockArgs(configuration=["/path/to/config1", "/path/to/config2"])
# Set logger level to capture INFO messages
with (
caplog.at_level(logging.INFO),
patch("esphome.writer.clean_all") as mock_clean_all,
):
result = command_clean_all(args)
assert result == 0
mock_clean_all.assert_called_once_with(["/path/to/config1", "/path/to/config2"])
# Check that success message was logged
assert "Done!" in caplog.text
def test_command_clean_all_oserror(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_all when writer.clean_all() raises OSError."""
args = MockArgs(configuration=["/path/to/config1"])
# Create a mock OSError with a specific message
mock_error = OSError("Permission denied: cannot delete directory")
# Set logger level to capture ERROR and INFO messages
with (
caplog.at_level(logging.INFO),
patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all,
):
result = command_clean_all(args)
assert result == 1
mock_clean_all.assert_called_once_with(["/path/to/config1"])
# Check that error message was logged
assert (
"Error cleaning all files: Permission denied: cannot delete directory"
in caplog.text
)
# Should not have success message
assert "Done!" not in caplog.text
def test_command_clean_all_oserror_no_message(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_all when writer.clean_all() raises OSError without message."""
args = MockArgs(configuration=["/path/to/config1"])
# Create a mock OSError without a message
mock_error = OSError()
# Set logger level to capture ERROR and INFO messages
with (
caplog.at_level(logging.INFO),
patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all,
):
result = command_clean_all(args)
assert result == 1
mock_clean_all.assert_called_once_with(["/path/to/config1"])
# Check that error message was logged (should show empty string for OSError without message)
assert "Error cleaning all files:" in caplog.text
# Should not have success message
assert "Done!" not in caplog.text
def test_command_clean_all_args_used() -> None:
"""Test that command_clean_all uses args.configuration parameter."""
# Test with different configuration paths
args1 = MockArgs(configuration=["/path/to/config1"])
args2 = MockArgs(configuration=["/path/to/config2", "/path/to/config3"])
with patch("esphome.writer.clean_all") as mock_clean_all:
result1 = command_clean_all(args1)
result2 = command_clean_all(args2)
assert result1 == 0
assert result2 == 0
assert mock_clean_all.call_count == 2
# Verify the correct configuration paths were passed
mock_clean_all.assert_any_call(["/path/to/config1"])
mock_clean_all.assert_any_call(["/path/to/config2", "/path/to/config3"])

View File

@@ -362,11 +362,17 @@ def test_clean_build(
assert dependencies_lock.exists()
assert platformio_cache_dir.exists()
# Mock PlatformIO's get_project_cache_dir
# Mock PlatformIO's ProjectConfig cache_dir
with patch(
"platformio.project.helpers.get_project_cache_dir"
) as mock_get_cache_dir:
mock_get_cache_dir.return_value = str(platformio_cache_dir)
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
mock_config.get.side_effect = (
lambda section, option: str(platformio_cache_dir)
if (section, option) == ("platformio", "cache_dir")
else ""
)
# Call the function
with caplog.at_level("INFO"):
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
# Mock import error for platformio
with (
patch.dict("sys.modules", {"platformio.project.helpers": None}),
patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"),
):
# Call the function
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
# Verify pioenvs exists before
assert pioenvs_dir.exists()
# Mock PlatformIO's get_project_cache_dir to return whitespace
# Mock PlatformIO's ProjectConfig cache_dir to return whitespace
with patch(
"platformio.project.helpers.get_project_cache_dir"
) as mock_get_cache_dir:
mock_get_cache_dir.return_value = " " # Whitespace only
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
mock_config.get.side_effect = (
lambda section, option: " " # Whitespace only
if (section, option) == ("platformio", "cache_dir")
else ""
)
# Call the function
with caplog.at_level("INFO"):
@@ -723,3 +735,135 @@ def test_write_cpp_with_duplicate_markers(
# Call should raise an error
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
write_cpp("// New code")
@patch("esphome.writer.CORE")
def test_clean_all(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all removes build and PlatformIO dirs."""
# Create build directories for multiple configurations
config1_dir = tmp_path / "config1"
config2_dir = tmp_path / "config2"
config1_dir.mkdir()
config2_dir.mkdir()
build_dir1 = config1_dir / ".esphome"
build_dir2 = config2_dir / ".esphome"
build_dir1.mkdir()
build_dir2.mkdir()
(build_dir1 / "dummy.txt").write_text("x")
(build_dir2 / "dummy.txt").write_text("x")
# Create PlatformIO directories
pio_cache = tmp_path / "pio_cache"
pio_packages = tmp_path / "pio_packages"
pio_platforms = tmp_path / "pio_platforms"
pio_core = tmp_path / "pio_core"
for d in (pio_cache, pio_packages, pio_platforms, pio_core):
d.mkdir()
(d / "keep").write_text("x")
# Mock ProjectConfig
with patch(
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
def cfg_get(section: str, option: str) -> str:
mapping = {
("platformio", "cache_dir"): str(pio_cache),
("platformio", "packages_dir"): str(pio_packages),
("platformio", "platforms_dir"): str(pio_platforms),
("platformio", "core_dir"): str(pio_core),
}
return mapping.get((section, option), "")
mock_config.get.side_effect = cfg_get
# Call
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(config1_dir), str(config2_dir)])
# Verify deletions
assert not build_dir1.exists()
assert not build_dir2.exists()
assert not pio_cache.exists()
assert not pio_packages.exists()
assert not pio_platforms.exists()
assert not pio_core.exists()
# Verify logging mentions each
assert "Deleting" in caplog.text
assert str(build_dir1) in caplog.text
assert str(build_dir2) in caplog.text
assert "PlatformIO cache" in caplog.text
assert "PlatformIO packages" in caplog.text
assert "PlatformIO platforms" in caplog.text
assert "PlatformIO core" in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_platformio_not_available(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all when PlatformIO is not available."""
# Build dirs
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
# PlatformIO dirs that should remain untouched
pio_cache = tmp_path / "pio_cache"
pio_cache.mkdir()
from esphome.writer import clean_all
with (
patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"),
):
clean_all([str(config_dir)])
# Build dir removed, PlatformIO dirs remain
assert not build_dir.exists()
assert pio_cache.exists()
# No PlatformIO-specific logs
assert "PlatformIO" not in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_partial_exists(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_all when only some build dirs exist."""
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
with patch(
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
# Return non-existent dirs
mock_config.get.side_effect = lambda *_args, **_kw: str(
tmp_path / "does_not_exist"
)
from esphome.writer import clean_all
clean_all([str(config_dir)])
assert not build_dir.exists()