From 0cc03dfe327a577d624005381b105f69fa200900 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:35:19 +1200 Subject: [PATCH 1/6] [json] Parsing json without a lambda (#10838) --- esphome/components/json/json_util.cpp | 19 +++++++++++++------ esphome/components/json/json_util.h | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) 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 { From 44767c32cf6c707df4918a9b1aed5b4da9e129cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 26 Sep 2025 01:08:16 +1200 Subject: [PATCH 2/6] Pin SHA for github actions (#10879) --- .github/actions/build-image/action.yaml | 4 +- .github/actions/restore-python/action.yml | 4 +- .github/workflows/auto-label-pr.yml | 6 +-- .github/workflows/ci-api-proto.yml | 10 ++--- .github/workflows/ci-clang-tidy-hash.yml | 8 ++-- .github/workflows/ci-docker.yml | 6 +-- .github/workflows/ci.yml | 42 +++++++++---------- .../workflows/codeowner-review-request.yml | 2 +- .github/workflows/codeql.yml | 6 +-- .github/workflows/external-component-bot.yml | 2 +- .github/workflows/issue-codeowner-notify.yml | 2 +- .github/workflows/release.yml | 34 +++++++-------- .github/workflows/stale.yml | 4 +- .github/workflows/status-check-labels.yml | 2 +- .github/workflows/sync-device-classes.yml | 8 ++-- 15 files changed, 70 insertions(+), 70 deletions(-) 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 ac7a810576..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.3.0 + 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 146da67e00..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.3.0 + 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.3.0 + 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.3.0 + 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.3.0 + 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.3.0 + 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..943a35b32a 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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 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 From f7ed127182368419f4ecf235e5852864627a2bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20L=C3=A9p=C3=A9e?= <205357+alepee@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:38:31 +0200 Subject: [PATCH 3/6] Add WTS01 temperature sensor component (#8539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antoine Lépée Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/wts01/__init__.py | 0 esphome/components/wts01/sensor.py | 41 +++++++++ esphome/components/wts01/wts01.cpp | 91 +++++++++++++++++++ esphome/components/wts01/wts01.h | 27 ++++++ tests/components/wts01/common.yaml | 7 ++ tests/components/wts01/test.esp32-ard.yaml | 5 + tests/components/wts01/test.esp32-c3-ard.yaml | 5 + tests/components/wts01/test.esp32-c3-idf.yaml | 5 + tests/components/wts01/test.esp32-idf.yaml | 5 + tests/components/wts01/test.esp8266-ard.yaml | 5 + tests/components/wts01/test.rp2040-ard.yaml | 5 + 12 files changed, 197 insertions(+) create mode 100644 esphome/components/wts01/__init__.py create mode 100644 esphome/components/wts01/sensor.py create mode 100644 esphome/components/wts01/wts01.cpp create mode 100644 esphome/components/wts01/wts01.h create mode 100644 tests/components/wts01/common.yaml create mode 100644 tests/components/wts01/test.esp32-ard.yaml create mode 100644 tests/components/wts01/test.esp32-c3-ard.yaml create mode 100644 tests/components/wts01/test.esp32-c3-idf.yaml create mode 100644 tests/components/wts01/test.esp32-idf.yaml create mode 100644 tests/components/wts01/test.esp8266-ard.yaml create mode 100644 tests/components/wts01/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e91116795a..b35b11c705 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -533,6 +533,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/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/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 From 65a1d2b2ff02728008808024cefbdb4e57277a48 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:13:27 -0400 Subject: [PATCH 4/6] [scd30] Fix temp offset (#10847) --- esphome/components/scd30/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 549626bee2b352508a486091b0fe0bfd3b6deebc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Sep 2025 10:39:56 -0500 Subject: [PATCH 5/6] Fix flakey password auth failure integration test (#10883) --- .../test_host_mode_api_password.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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() + ) From 74f09a2b5986d1b05d71db22e38cf6250bdaa970 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:55:43 -0400 Subject: [PATCH 6/6] [core] Rename to clean-platform to clean-all (#10876) --- esphome/__main__.py | 19 ++++----- esphome/dashboard/web_server.py | 10 +++-- esphome/writer.py | 10 +++-- tests/unit_tests/test_main.py | 74 +++++++++++++++------------------ tests/unit_tests/test_writer.py | 61 +++++++++++++++------------ 5 files changed, 89 insertions(+), 85 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 27aced5f33..42880e6cfc 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -731,11 +731,11 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None: return clean_mqtt(config, args) -def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None: +def command_clean_all(args: ArgsProtocol) -> int | None: try: - writer.clean_platform() + writer.clean_all(args.configuration) except OSError as err: - _LOGGER.error("Error deleting platform files: %s", err) + _LOGGER.error("Error cleaning all files: %s", err) return 1 _LOGGER.info("Done!") return 0 @@ -931,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 = { @@ -941,7 +942,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "clean-platform": command_clean_platform, "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, @@ -951,7 +951,6 @@ POST_CONFIG_ACTIONS = { SIMPLE_CONFIG_ACTIONS = [ "clean", "clean-mqtt", - "clean-platform", "config", ] @@ -1156,11 +1155,9 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs="+" ) - parser_clean = subparsers.add_parser( - "clean-platform", help="Delete all platform files." - ) - parser_clean.add_argument( - "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( @@ -1209,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/dashboard/web_server.py b/esphome/dashboard/web_server.py index a4c24369a3..e3a0013e2f 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -479,10 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] -class EsphomeCleanPlatformHandler(EsphomeCommandWebSocket): +class EsphomeCleanAllHandler(EsphomeCommandWebSocket): async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - return [*DASHBOARD_COMMAND, "clean-platform", config_file] + 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): @@ -1319,7 +1321,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-platform", EsphomeCleanPlatformHandler), + (f"{rel}clean-all", EsphomeCleanAllHandler), (f"{rel}clean", EsphomeCleanHandler), (f"{rel}vscode", EsphomeVscodeHandler), (f"{rel}ace", EsphomeAceEditorHandler), diff --git a/esphome/writer.py b/esphome/writer.py index 718041876a..403cd8165d 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -335,13 +335,15 @@ def clean_build(): shutil.rmtree(cache_dir) -def clean_platform(): +def clean_all(configuration: list[str]): import shutil # Clean entire build dir - if CORE.build_path.is_dir(): - _LOGGER.info("Deleting %s", CORE.build_path) - shutil.rmtree(CORE.build_path) + 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: diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 8799ac56ff..e35378145a 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -17,7 +17,7 @@ from esphome import platformio_api from esphome.__main__ import ( Purpose, choose_upload_log_host, - command_clean_platform, + command_clean_all, command_rename, command_update_all, command_wizard, @@ -1857,33 +1857,31 @@ esp32: assert "can only concatenate str" not in clean_output -def test_command_clean_platform_success( +def test_command_clean_all_success( caplog: pytest.LogCaptureFixture, ) -> None: - """Test command_clean_platform when writer.clean_platform() succeeds.""" - args = MockArgs() - config = {} + """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_platform") as mock_clean_platform, + patch("esphome.writer.clean_all") as mock_clean_all, ): - result = command_clean_platform(args, config) + result = command_clean_all(args) assert result == 0 - mock_clean_platform.assert_called_once() + 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_platform_oserror( +def test_command_clean_all_oserror( caplog: pytest.LogCaptureFixture, ) -> None: - """Test command_clean_platform when writer.clean_platform() raises OSError.""" - args = MockArgs() - config = {} + """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") @@ -1891,30 +1889,27 @@ def test_command_clean_platform_oserror( # Set logger level to capture ERROR and INFO messages with ( caplog.at_level(logging.INFO), - patch( - "esphome.writer.clean_platform", side_effect=mock_error - ) as mock_clean_platform, + patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all, ): - result = command_clean_platform(args, config) + result = command_clean_all(args) assert result == 1 - mock_clean_platform.assert_called_once() + mock_clean_all.assert_called_once_with(["/path/to/config1"]) # Check that error message was logged assert ( - "Error deleting platform files: Permission denied: cannot delete directory" + "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_platform_oserror_no_message( +def test_command_clean_all_oserror_no_message( caplog: pytest.LogCaptureFixture, ) -> None: - """Test command_clean_platform when writer.clean_platform() raises OSError without message.""" - args = MockArgs() - config = {} + """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() @@ -1922,34 +1917,33 @@ def test_command_clean_platform_oserror_no_message( # Set logger level to capture ERROR and INFO messages with ( caplog.at_level(logging.INFO), - patch( - "esphome.writer.clean_platform", side_effect=mock_error - ) as mock_clean_platform, + patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all, ): - result = command_clean_platform(args, config) + result = command_clean_all(args) assert result == 1 - mock_clean_platform.assert_called_once() + 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 deleting platform files:" in caplog.text + assert "Error cleaning all files:" in caplog.text # Should not have success message assert "Done!" not in caplog.text -def test_command_clean_platform_args_and_config_ignored() -> None: - """Test that command_clean_platform ignores args and config parameters.""" - # Test with various args and config to ensure they don't affect the function - args1 = MockArgs(name="test1", file="test.bin") - config1 = {"wifi": {"ssid": "test"}} +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"]) - args2 = MockArgs(name="test2", dashboard=True) - config2 = {"api": {}, "ota": {}} - - with patch("esphome.writer.clean_platform") as mock_clean_platform: - result1 = command_clean_platform(args1, config1) - result2 = command_clean_platform(args2, config2) + 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_platform.call_count == 2 + 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 dc5fbf8db5..66e0b6cb67 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -738,16 +738,24 @@ def test_write_cpp_with_duplicate_markers( @patch("esphome.writer.CORE") -def test_clean_platform( +def test_clean_all( mock_core: MagicMock, tmp_path: Path, caplog: pytest.LogCaptureFixture, ) -> None: - """Test clean_platform removes build and PlatformIO dirs.""" - # Create build directory - build_dir = tmp_path / "build" - build_dir.mkdir() - (build_dir / "dummy.txt").write_text("x") + """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" @@ -758,9 +766,6 @@ def test_clean_platform( d.mkdir() (d / "keep").write_text("x") - # Setup CORE - mock_core.build_path = build_dir - # Mock ProjectConfig with patch( "platformio.project.config.ProjectConfig.get_instance" @@ -780,13 +785,14 @@ def test_clean_platform( mock_config.get.side_effect = cfg_get # Call - from esphome.writer import clean_platform + from esphome.writer import clean_all with caplog.at_level("INFO"): - clean_platform() + clean_all([str(config1_dir), str(config2_dir)]) # Verify deletions - assert not build_dir.exists() + 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() @@ -794,7 +800,8 @@ def test_clean_platform( # Verify logging mentions each assert "Deleting" in caplog.text - assert str(build_dir) 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 @@ -802,28 +809,29 @@ def test_clean_platform( @patch("esphome.writer.CORE") -def test_clean_platform_platformio_not_available( +def test_clean_all_platformio_not_available( mock_core: MagicMock, tmp_path: Path, caplog: pytest.LogCaptureFixture, ) -> None: - """Test clean_platform when PlatformIO is not available.""" - # Build dir - build_dir = tmp_path / "build" + """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() - mock_core.build_path = build_dir # PlatformIO dirs that should remain untouched pio_cache = tmp_path / "pio_cache" pio_cache.mkdir() - from esphome.writer import clean_platform + from esphome.writer import clean_all with ( patch.dict("sys.modules", {"platformio.project.config": None}), caplog.at_level("INFO"), ): - clean_platform() + clean_all([str(config_dir)]) # Build dir removed, PlatformIO dirs remain assert not build_dir.exists() @@ -834,14 +842,15 @@ def test_clean_platform_platformio_not_available( @patch("esphome.writer.CORE") -def test_clean_platform_partial_exists( +def test_clean_all_partial_exists( mock_core: MagicMock, tmp_path: Path, ) -> None: - """Test clean_platform when only build dir exists.""" - build_dir = tmp_path / "build" + """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() - mock_core.build_path = build_dir with patch( "platformio.project.config.ProjectConfig.get_instance" @@ -853,8 +862,8 @@ def test_clean_platform_partial_exists( tmp_path / "does_not_exist" ) - from esphome.writer import clean_platform + from esphome.writer import clean_all - clean_platform() + clean_all([str(config_dir)]) assert not build_dir.exists()