diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 403b9d8c2a..9c7f051e05 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -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 diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 5d290894a7..f314e79ad9 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -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 diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 66369c706f..1670bd1821 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -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: | diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index ec214d1a77..c122859442 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -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({ diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 2f47386abf..8760a1aaa5 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -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({ diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 915a4dfb7e..7111c61dda 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -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: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a89a12e2e7..f4f7f8bd82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 475e05b970..563d55f42b 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -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; diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7a7c39aeec..e31e547b75 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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}}" diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 736c986f7e..4fa020f63d 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -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: | diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index ab9b96b45a..6faf956c87 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -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; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efc8424cd6..4d003df5ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: | diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 88e07d3f58..da1e62c8e1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -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 diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index 675be49c27..e44fd18132 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -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({ diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index c14071ea62..9479645ccc 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cab433c7f9..818f360860 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CODEOWNERS b/CODEOWNERS index 77a837df0d..3747acd2b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/esphome/__main__.py b/esphome/__main__.py index aa237c83a7..42880e6cfc 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -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") diff --git a/esphome/automation.py b/esphome/automation.py index 99d4362845..99def9f273 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -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) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5fb84d3c21..394d3ead43 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -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, ) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index ad99de4b4a..9273cca2d3 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -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 { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a27adfe241..30b98803d1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -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 ¤t_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(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(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_(); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 380894a4ff..cc7e4d6895 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -10,8 +10,8 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" -#include #include +#include 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 diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 50c43b96fd..633f39b552 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -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 diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 245933724b..410ba2334e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -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: diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 248a4b1f82..ee8472b21c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -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 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 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; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index ac43af6d54..a5494168f9 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string(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(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(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"); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4afc66dc44..ccbd781431 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -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 diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 9379dfee7d..1afcba6664 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -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 diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f38f4a31a..ff94b92b5d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -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(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 diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 8b5e624df2..354c764825 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -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); } diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 44f9eee571..0c6e49d6ca 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -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 &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 &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 void call_homeassistant_service(const std::string &service_name) { diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 5df9c7c792..4026741ee4 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -3,10 +3,10 @@ #include "api_server.h" #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES +#include #include "api_pb2.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include namespace esphome::api { @@ -62,7 +62,7 @@ template class HomeAssistantServiceCallAction : public Actionservice_.value(x...); resp.set_service(StringRef(service_value)); resp.is_event = this->is_event_; @@ -84,7 +84,7 @@ template class HomeAssistantServiceCallAction : public Actionparent_->send_homeassistant_service_call(resp); + this->parent_->send_homeassistant_action(resp); } protected: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 0e5ec61050..9d780692ec 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -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(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; diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index f21b5028c7..42a88f1421 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -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), ), } ) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 540492f8c5..cde82fbfb0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -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(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(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); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index e5d5ff2dd6..60bbc93e8b 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -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); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 532aff550e..cd7261d5e5 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -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); } diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index c0f0ca2fe0..89181d27b4 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -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], diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp index 7e21122087..55a3f0b96c 100644 --- a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp @@ -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(arg); uint8_t *buffer = that->output_->get_data(); size_t buffer_length = that->output_->get_max_size(); diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h index b585252584..0ede366e73 100644 --- a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32_CAMERA_JPEG_ENCODER #include @@ -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_{}; diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 8886dc415b..dae9799028 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -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), ), diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 8655d5a02a..787fb9fb65 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -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) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 4f83bf2435..ec6eac670f 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -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"]) diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 87bf6727f2..c9fb006568 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -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 diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index b3300335b9..8feec26fe6 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -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 diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index f2e7ae93cb..a7aae16f17 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -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(&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(&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(&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; } diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 8533875d43..9b3831b784 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -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; diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 51c0fcf9cb..643f23f499 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -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()); + // 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(); - 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) } diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 69b809ec49..0349833342 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -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 { diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py index 49a75da232..a38493e816 100644 --- a/esphome/components/mipi_rgb/models/waveshare.py +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -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", diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index d712e2401d..f0d1044f3f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -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); diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 6981af4de9..194df8ec4f 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -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( diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index c5466eb1f0..d2ff4da068 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -5,7 +5,10 @@ #include "esphome/core/component.h" #include #include "usb/usb_host.h" - +#include +#include +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #include 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 &data = {}); + // Lock-free event queue and pool for USB task to main loop communication + // Must be public for access from static callbacks + LockFreeQueue event_queue; + EventPool 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}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c0c12fa18..5c9d56c7f9 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -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(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(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(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(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. diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 74e7933824..601bfe7366 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -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(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))) diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index f7d60c307a..35834c7529 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -100,12 +100,12 @@ std::vector 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); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index bf1c9086f1..8603e28d62 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -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(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(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); } } diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index a103c51add..b41e0a52e9 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -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 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 { friend class USBUartComponent; friend class USBUartTypeCdcAcm; @@ -90,16 +105,20 @@ class USBUartChannel : public uart::UARTComponent, public Parenteddummy_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 input_started_{true}; + std::atomic output_started_{true}; + std::atomic 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 usb_data_queue_; + EventPool chunk_pool_; + protected: std::vector channels_{}; }; diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 2aff405036..a82ec462d9 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -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") diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ef74c14924..a784123006 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -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, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 43ece636e5..8c7b55c274 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -3,7 +3,7 @@ #include #include -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) #include #else @@ -11,7 +11,7 @@ #endif #endif -#if defined(USE_ESP32) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) #include #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 phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, {ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index bbe1bbb874..ee62ec1a69 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -20,7 +20,7 @@ #include #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 #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 diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp deleted file mode 100644 index 89298e07c7..0000000000 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ /dev/null @@ -1,860 +0,0 @@ -#include "wifi_component.h" - -#ifdef USE_WIFI -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include -#include - -#include -#include -#ifdef USE_WIFI_WPA2_EAP -#include -#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 sta, optional 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(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(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(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, ¤t_conf); - if (err != ERR_OK) { - ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err)); - // can continue - } - - if (memcmp(¤t_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 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(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 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(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(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 diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 31ee712a48..aa0a993e79 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1,7 +1,7 @@ #include "wifi_component.h" #ifdef USE_WIFI -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } // namespace wifi } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 #endif diff --git a/esphome/components/wts01/__init__.py b/esphome/components/wts01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wts01/sensor.py b/esphome/components/wts01/sensor.py new file mode 100644 index 0000000000..bf4f0262ad --- /dev/null +++ b/esphome/components/wts01/sensor.py @@ -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) diff --git a/esphome/components/wts01/wts01.cpp b/esphome/components/wts01/wts01.cpp new file mode 100644 index 0000000000..cb910d89cf --- /dev/null +++ b/esphome/components/wts01/wts01.cpp @@ -0,0 +1,91 @@ +#include "wts01.h" +#include "esphome/core/log.h" +#include + +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(temp) + (sign * static_cast(this->buffer_[7]) / 100.0f); + + ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature); + + this->publish_state(temperature); +} + +} // namespace wts01 +} // namespace esphome diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h new file mode 100644 index 0000000000..298595a5d6 --- /dev/null +++ b/esphome/components/wts01/wts01.h @@ -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 diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 12c4ee0c0d..feaf6e2d42 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -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); } diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 5d908b328c..ea6837888b 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -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 home_id_{0, 0, 0, 0}; // Fixed buffer for home ID - std::array 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 buffer_; // Fixed buffer for incoming data + std::array 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) diff --git a/esphome/core/config.py b/esphome/core/config.py index 6d4f5af692..7bf7f82a8b 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -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) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a0a621dd4f..ef93fd0b65 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 1af2fd8907..4eb6a89f53 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -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 diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 291592dd2b..b2022c7ae6 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -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: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 7b6e6b4507..b5601c9e0f 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -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), diff --git a/esphome/types.py b/esphome/types.py index 62499a953c..c474d0d076 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -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.""" diff --git a/esphome/util.py b/esphome/util.py index 3bf3248cb3..d41800dc20 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -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 diff --git a/esphome/writer.py b/esphome/writer.py index 6d34d8f751..403cd8165d 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 2424db1639..927b831b1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/requirements_test.txt b/requirements_test.txt index 2c78eadf45..59ea77fd2d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -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 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index fa04222c5d..487c187372 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -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" diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 7ac11e4da6..4f1693dac8 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -13,7 +13,6 @@ esphome: api: port: 8000 - password: pwd reboot_timeout: 0min encryption: key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= diff --git a/tests/components/wts01/common.yaml b/tests/components/wts01/common.yaml new file mode 100644 index 0000000000..c26cc3e475 --- /dev/null +++ b/tests/components/wts01/common.yaml @@ -0,0 +1,7 @@ +uart: + rx_pin: ${rx_pin} + baud_rate: 9600 + +sensor: + - platform: wts01 + id: wts01_sensor diff --git a/tests/components/wts01/test.esp32-ard.yaml b/tests/components/wts01/test.esp32-ard.yaml new file mode 100644 index 0000000000..4904e1f54f --- /dev/null +++ b/tests/components/wts01/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO16 + rx_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/wts01/test.esp32-c3-ard.yaml b/tests/components/wts01/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..00cec5b3b8 --- /dev/null +++ b/tests/components/wts01/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO6 + rx_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/components/wts01/test.esp32-c3-idf.yaml b/tests/components/wts01/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..00cec5b3b8 --- /dev/null +++ b/tests/components/wts01/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO6 + rx_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/components/wts01/test.esp32-idf.yaml b/tests/components/wts01/test.esp32-idf.yaml new file mode 100644 index 0000000000..4904e1f54f --- /dev/null +++ b/tests/components/wts01/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO16 + rx_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/wts01/test.esp8266-ard.yaml b/tests/components/wts01/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3b44f9c9c3 --- /dev/null +++ b/tests/components/wts01/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/wts01/test.rp2040-ard.yaml b/tests/components/wts01/test.rp2040-ard.yaml new file mode 100644 index 0000000000..16b2a4b006 --- /dev/null +++ b/tests/components/wts01/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +<<: !include common.yaml diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py index 825c2c55f2..5c5e689e45 100644 --- a/tests/integration/test_host_mode_api_password.py +++ b/tests/integration/test_host_mode_api_password.py @@ -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() + ) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index bb047d063c..e35378145a 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -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"]) diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index ba309f2406..66e0b6cb67 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -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()