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

Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston
2025-09-25 10:56:43 -05:00
36 changed files with 392 additions and 166 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,13 +43,13 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6.0.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - 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 - name: Set TAG
run: | run: |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -738,11 +738,11 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args) return clean_mqtt(config, args)
def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None: def command_clean_all(args: ArgsProtocol) -> int | None:
try: try:
writer.clean_platform() writer.clean_all(args.configuration)
except OSError as err: except OSError as err:
_LOGGER.error("Error deleting platform files: %s", err) _LOGGER.error("Error cleaning all files: %s", err)
return 1 return 1
_LOGGER.info("Done!") _LOGGER.info("Done!")
return 0 return 0
@@ -938,6 +938,7 @@ PRE_CONFIG_ACTIONS = {
"dashboard": command_dashboard, "dashboard": command_dashboard,
"vscode": command_vscode, "vscode": command_vscode,
"update-all": command_update_all, "update-all": command_update_all,
"clean-all": command_clean_all,
} }
POST_CONFIG_ACTIONS = { POST_CONFIG_ACTIONS = {
@@ -948,7 +949,6 @@ POST_CONFIG_ACTIONS = {
"run": command_run, "run": command_run,
"clean": command_clean, "clean": command_clean,
"clean-mqtt": command_clean_mqtt, "clean-mqtt": command_clean_mqtt,
"clean-platform": command_clean_platform,
"mqtt-fingerprint": command_mqtt_fingerprint, "mqtt-fingerprint": command_mqtt_fingerprint,
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
@@ -958,7 +958,6 @@ POST_CONFIG_ACTIONS = {
SIMPLE_CONFIG_ACTIONS = [ SIMPLE_CONFIG_ACTIONS = [
"clean", "clean",
"clean-mqtt", "clean-mqtt",
"clean-platform",
"config", "config",
] ]
@@ -1174,11 +1173,9 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+" "configuration", help="Your YAML configuration file(s).", nargs="+"
) )
parser_clean = subparsers.add_parser( parser_clean_all = subparsers.add_parser("clean-all", help="Clean all files.")
"clean-platform", help="Delete all platform files." parser_clean_all.add_argument(
) "configuration", help="Your YAML configuration directory.", nargs="*"
parser_clean.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
) )
parser_dashboard = subparsers.add_parser( parser_dashboard = subparsers.add_parser(
@@ -1227,7 +1224,7 @@ def parse_args(argv):
parser_update = subparsers.add_parser("update-all") parser_update = subparsers.add_parser("update-all")
parser_update.add_argument( 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") parser_idedata = subparsers.add_parser("idedata")

View File

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

View File

@@ -49,6 +49,8 @@ std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid. /// 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); 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 /// Builder class for creating JSON documents without lambdas
class JsonBuilder { class JsonBuilder {

View File

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

View File

View File

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

View File

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

View File

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

View File

@@ -479,10 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] 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]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) clean_build_dir = json_message.get("clean_build_dir", True)
return [*DASHBOARD_COMMAND, "clean-platform", config_file] if clean_build_dir:
return [*DASHBOARD_COMMAND, "clean-all", settings.config_dir]
return [*DASHBOARD_COMMAND, "clean-all"]
class EsphomeCleanHandler(EsphomeCommandWebSocket): 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}compile", EsphomeCompileHandler),
(f"{rel}validate", EsphomeValidateHandler), (f"{rel}validate", EsphomeValidateHandler),
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
(f"{rel}clean-platform", EsphomeCleanPlatformHandler), (f"{rel}clean-all", EsphomeCleanAllHandler),
(f"{rel}clean", EsphomeCleanHandler), (f"{rel}clean", EsphomeCleanHandler),
(f"{rel}vscode", EsphomeVscodeHandler), (f"{rel}vscode", EsphomeVscodeHandler),
(f"{rel}ace", EsphomeAceEditorHandler), (f"{rel}ace", EsphomeAceEditorHandler),

View File

@@ -335,13 +335,15 @@ def clean_build():
shutil.rmtree(cache_dir) shutil.rmtree(cache_dir)
def clean_platform(): def clean_all(configuration: list[str]):
import shutil import shutil
# Clean entire build dir # Clean entire build dir
if CORE.build_path.is_dir(): for dir in configuration:
_LOGGER.info("Deleting %s", CORE.build_path) buid_dir = Path(dir) / ".esphome"
shutil.rmtree(CORE.build_path) if buid_dir.is_dir():
_LOGGER.info("Deleting %s", buid_dir)
shutil.rmtree(buid_dir)
# Clean PlatformIO project files # Clean PlatformIO project files
try: try:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ from esphome import platformio_api
from esphome.__main__ import ( from esphome.__main__ import (
Purpose, Purpose,
choose_upload_log_host, choose_upload_log_host,
command_clean_platform, command_clean_all,
command_rename, command_rename,
command_update_all, command_update_all,
command_wizard, command_wizard,
@@ -1857,33 +1857,31 @@ esp32:
assert "can only concatenate str" not in clean_output assert "can only concatenate str" not in clean_output
def test_command_clean_platform_success( def test_command_clean_all_success(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test command_clean_platform when writer.clean_platform() succeeds.""" """Test command_clean_all when writer.clean_all() succeeds."""
args = MockArgs() args = MockArgs(configuration=["/path/to/config1", "/path/to/config2"])
config = {}
# Set logger level to capture INFO messages # Set logger level to capture INFO messages
with ( with (
caplog.at_level(logging.INFO), 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 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 # Check that success message was logged
assert "Done!" in caplog.text assert "Done!" in caplog.text
def test_command_clean_platform_oserror( def test_command_clean_all_oserror(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test command_clean_platform when writer.clean_platform() raises OSError.""" """Test command_clean_all when writer.clean_all() raises OSError."""
args = MockArgs() args = MockArgs(configuration=["/path/to/config1"])
config = {}
# Create a mock OSError with a specific message # Create a mock OSError with a specific message
mock_error = OSError("Permission denied: cannot delete directory") 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 # Set logger level to capture ERROR and INFO messages
with ( with (
caplog.at_level(logging.INFO), caplog.at_level(logging.INFO),
patch( patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all,
"esphome.writer.clean_platform", side_effect=mock_error
) as mock_clean_platform,
): ):
result = command_clean_platform(args, config) result = command_clean_all(args)
assert result == 1 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 # Check that error message was logged
assert ( assert (
"Error deleting platform files: Permission denied: cannot delete directory" "Error cleaning all files: Permission denied: cannot delete directory"
in caplog.text in caplog.text
) )
# Should not have success message # Should not have success message
assert "Done!" not in caplog.text 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, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test command_clean_platform when writer.clean_platform() raises OSError without message.""" """Test command_clean_all when writer.clean_all() raises OSError without message."""
args = MockArgs() args = MockArgs(configuration=["/path/to/config1"])
config = {}
# Create a mock OSError without a message # Create a mock OSError without a message
mock_error = OSError() mock_error = OSError()
@@ -1922,34 +1917,33 @@ def test_command_clean_platform_oserror_no_message(
# Set logger level to capture ERROR and INFO messages # Set logger level to capture ERROR and INFO messages
with ( with (
caplog.at_level(logging.INFO), caplog.at_level(logging.INFO),
patch( patch("esphome.writer.clean_all", side_effect=mock_error) as mock_clean_all,
"esphome.writer.clean_platform", side_effect=mock_error
) as mock_clean_platform,
): ):
result = command_clean_platform(args, config) result = command_clean_all(args)
assert result == 1 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) # 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 # Should not have success message
assert "Done!" not in caplog.text assert "Done!" not in caplog.text
def test_command_clean_platform_args_and_config_ignored() -> None: def test_command_clean_all_args_used() -> None:
"""Test that command_clean_platform ignores args and config parameters.""" """Test that command_clean_all uses args.configuration parameter."""
# Test with various args and config to ensure they don't affect the function # Test with different configuration paths
args1 = MockArgs(name="test1", file="test.bin") args1 = MockArgs(configuration=["/path/to/config1"])
config1 = {"wifi": {"ssid": "test"}} args2 = MockArgs(configuration=["/path/to/config2", "/path/to/config3"])
args2 = MockArgs(name="test2", dashboard=True) with patch("esphome.writer.clean_all") as mock_clean_all:
config2 = {"api": {}, "ota": {}} result1 = command_clean_all(args1)
result2 = command_clean_all(args2)
with patch("esphome.writer.clean_platform") as mock_clean_platform:
result1 = command_clean_platform(args1, config1)
result2 = command_clean_platform(args2, config2)
assert result1 == 0 assert result1 == 0
assert result2 == 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"])

View File

@@ -739,16 +739,24 @@ def test_write_cpp_with_duplicate_markers(
@patch("esphome.writer.CORE") @patch("esphome.writer.CORE")
def test_clean_platform( def test_clean_all(
mock_core: MagicMock, mock_core: MagicMock,
tmp_path: Path, tmp_path: Path,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test clean_platform removes build and PlatformIO dirs.""" """Test clean_all removes build and PlatformIO dirs."""
# Create build directory # Create build directories for multiple configurations
build_dir = tmp_path / "build" config1_dir = tmp_path / "config1"
build_dir.mkdir() config2_dir = tmp_path / "config2"
(build_dir / "dummy.txt").write_text("x") 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 # Create PlatformIO directories
pio_cache = tmp_path / "pio_cache" pio_cache = tmp_path / "pio_cache"
@@ -759,9 +767,6 @@ def test_clean_platform(
d.mkdir() d.mkdir()
(d / "keep").write_text("x") (d / "keep").write_text("x")
# Setup CORE
mock_core.build_path = build_dir
# Mock ProjectConfig # Mock ProjectConfig
with patch( with patch(
"platformio.project.config.ProjectConfig.get_instance" "platformio.project.config.ProjectConfig.get_instance"
@@ -781,13 +786,14 @@ def test_clean_platform(
mock_config.get.side_effect = cfg_get mock_config.get.side_effect = cfg_get
# Call # Call
from esphome.writer import clean_platform from esphome.writer import clean_all
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
clean_platform() clean_all([str(config1_dir), str(config2_dir)])
# Verify deletions # 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_cache.exists()
assert not pio_packages.exists() assert not pio_packages.exists()
assert not pio_platforms.exists() assert not pio_platforms.exists()
@@ -795,7 +801,8 @@ def test_clean_platform(
# Verify logging mentions each # Verify logging mentions each
assert "Deleting" in caplog.text 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 cache" in caplog.text
assert "PlatformIO packages" in caplog.text assert "PlatformIO packages" in caplog.text
assert "PlatformIO platforms" in caplog.text assert "PlatformIO platforms" in caplog.text
@@ -803,28 +810,29 @@ def test_clean_platform(
@patch("esphome.writer.CORE") @patch("esphome.writer.CORE")
def test_clean_platform_platformio_not_available( def test_clean_all_platformio_not_available(
mock_core: MagicMock, mock_core: MagicMock,
tmp_path: Path, tmp_path: Path,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test clean_platform when PlatformIO is not available.""" """Test clean_all when PlatformIO is not available."""
# Build dir # Build dirs
build_dir = tmp_path / "build" config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir() build_dir.mkdir()
mock_core.build_path = build_dir
# PlatformIO dirs that should remain untouched # PlatformIO dirs that should remain untouched
pio_cache = tmp_path / "pio_cache" pio_cache = tmp_path / "pio_cache"
pio_cache.mkdir() pio_cache.mkdir()
from esphome.writer import clean_platform from esphome.writer import clean_all
with ( with (
patch.dict("sys.modules", {"platformio.project.config": None}), patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"), caplog.at_level("INFO"),
): ):
clean_platform() clean_all([str(config_dir)])
# Build dir removed, PlatformIO dirs remain # Build dir removed, PlatformIO dirs remain
assert not build_dir.exists() assert not build_dir.exists()
@@ -835,14 +843,15 @@ def test_clean_platform_platformio_not_available(
@patch("esphome.writer.CORE") @patch("esphome.writer.CORE")
def test_clean_platform_partial_exists( def test_clean_all_partial_exists(
mock_core: MagicMock, mock_core: MagicMock,
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
"""Test clean_platform when only build dir exists.""" """Test clean_all when only some build dirs exist."""
build_dir = tmp_path / "build" config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir() build_dir.mkdir()
mock_core.build_path = build_dir
with patch( with patch(
"platformio.project.config.ProjectConfig.get_instance" "platformio.project.config.ProjectConfig.get_instance"
@@ -854,8 +863,8 @@ def test_clean_platform_partial_exists(
tmp_path / "does_not_exist" 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() assert not build_dir.exists()