From 9f17a82acf86bd867314b77c0df406911ae2b0e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 05:50:31 -0500 Subject: [PATCH 01/17] Revert "Pin SHA for all github actions" (#152229) --- .github/workflows/builder.yml | 44 ++--- .github/workflows/ci.yaml | 168 +++++++++--------- .github/workflows/codeql.yml | 6 +- .github/workflows/detect-duplicate-issues.yml | 8 +- .../workflows/detect-non-english-issues.yml | 6 +- .github/workflows/lock.yml | 2 +- .github/workflows/restrict-task-creation.yml | 2 +- .github/workflows/stale.yml | 6 +- .github/workflows/translations.yml | 4 +- .github/workflows/wheels.yml | 34 ++-- 10 files changed, 140 insertions(+), 140 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 81a327424feaea..63cafce6c731f4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -27,12 +27,12 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: translations path: translations.tar.gz @@ -90,11 +90,11 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 + uses: dawidd6/action-download-artifact@v11 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -105,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 + uses: dawidd6/action-download-artifact@v11 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: OHF-Voice/intents-package @@ -116,7 +116,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -175,7 +175,7 @@ jobs: sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - name: Download translations - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: translations @@ -190,14 +190,14 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to GitHub Container Registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0 + uses: home-assistant/builder@2025.03.0 with: args: | $BUILD_ARGS \ @@ -242,7 +242,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set build additional args run: | @@ -256,14 +256,14 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0 + uses: home-assistant/builder@2025.03.0 with: args: | $BUILD_ARGS \ @@ -279,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -321,23 +321,23 @@ jobs: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Install Cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + uses: sigstore/cosign-installer@v3.9.2 with: cosign-release: "v2.2.3" - name: Login to DockerHub if: matrix.registry == 'docker.io/homeassistant' - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@v3.5.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry if: matrix.registry == 'ghcr.io/home-assistant' - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -454,15 +454,15 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: translations @@ -480,7 +480,7 @@ jobs: python -m build - name: Upload package to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 41a2c1c7ea1ec6..0d465f428a6219 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: | @@ -120,7 +120,7 @@ jobs: run: | echo "key=$(lsb_release -rs)-apt-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}" >> $GITHUB_OUTPUT - name: Filter for core changes - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + uses: dorny/paths-filter@v3.0.2 id: core with: filters: .core_files.yaml @@ -135,7 +135,7 @@ jobs: echo "Result:" cat .integration_paths.yaml - name: Filter for integration changes - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + uses: dorny/paths-filter@v3.0.2 id: integrations with: filters: .integration_paths.yaml @@ -254,16 +254,16 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -279,7 +279,7 @@ jobs: uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true @@ -300,16 +300,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -318,7 +318,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -340,16 +340,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -358,7 +358,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -380,16 +380,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -398,7 +398,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -470,7 +470,7 @@ jobs: - script/hassfest/docker/Dockerfile steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -489,10 +489,10 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -505,7 +505,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -513,7 +513,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@v4.2.4 with: path: ${{ env.UV_CACHE_DIR }} key: >- @@ -585,7 +585,7 @@ jobs: python --version uv pip freeze >> pip_freeze.txt - name: Upload pip_freeze artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pip-freeze-${{ matrix.python-version }} path: pip_freeze.txt @@ -631,16 +631,16 @@ jobs: -o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \ libturbojpeg - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -664,16 +664,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -698,9 +698,9 @@ jobs: && github.event_name == 'pull_request' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Dependency review - uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 + uses: actions/dependency-review-action@v4.7.3 with: license-check: false # We use our own license audit checks @@ -721,16 +721,16 @@ jobs: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -742,7 +742,7 @@ jobs: . venv/bin/activate python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json - name: Upload licenses - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: licenses-${{ github.run_number }}-${{ matrix.python-version }} path: licenses-${{ matrix.python-version }}.json @@ -764,16 +764,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -811,16 +811,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -856,10 +856,10 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -872,7 +872,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -880,7 +880,7 @@ jobs: ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@v4.2.4 with: path: .mypy_cache key: >- @@ -947,16 +947,16 @@ jobs: libturbojpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -968,7 +968,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pytest_buckets path: pytest_buckets.txt @@ -1022,16 +1022,16 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1045,7 +1045,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Download pytest_buckets - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: pytest_buckets - name: Compile English translations @@ -1084,14 +1084,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1104,7 +1104,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml @@ -1169,16 +1169,16 @@ jobs: libmariadb-dev-compat \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1237,7 +1237,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1245,7 +1245,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1259,7 +1259,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: test-results-mariadb-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1325,16 +1325,16 @@ jobs: sudo apt-get -y install \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1394,7 +1394,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1402,7 +1402,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1416,7 +1416,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: test-results-postgres-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1437,14 +1437,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@v5.5.1 with: fail_ci_if_error: true flags: full-suite @@ -1498,16 +1498,16 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1563,14 +1563,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1583,7 +1583,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml @@ -1601,14 +1601,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@v5.5.1 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} @@ -1628,11 +1628,11 @@ jobs: timeout-minutes: 10 steps: - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: pattern: test-results-* - name: Upload test results to Codecov - uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + uses: codecov/test-results-action@v1 with: fail_ci_if_error: true verbose: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c3a5073d03898a..044aea8d2cff82 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,14 +21,14 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 + uses: github/codeql-action/init@v3.30.3 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 + uses: github/codeql-action/analyze@v3.30.3 with: category: "/language:python" diff --git a/.github/workflows/detect-duplicate-issues.yml b/.github/workflows/detect-duplicate-issues.yml index 801c4bb36bc95c..1997f1c02b0c63 100644 --- a/.github/workflows/detect-duplicate-issues.yml +++ b/.github/workflows/detect-duplicate-issues.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check if integration label was added and extract details id: extract - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 with: script: | // Debug: Log the event payload @@ -113,7 +113,7 @@ jobs: - name: Fetch similar issues id: fetch_similar if: steps.extract.outputs.should_continue == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 env: INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }} CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }} @@ -231,7 +231,7 @@ jobs: - name: Detect duplicates using AI id: ai_detection if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1 + uses: actions/ai-inference@v2.0.1 with: model: openai/gpt-4o system-prompt: | @@ -280,7 +280,7 @@ jobs: - name: Post duplicate detection results id: post_results if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 env: AI_RESPONSE: ${{ steps.ai_detection.outputs.response }} SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }} diff --git a/.github/workflows/detect-non-english-issues.yml b/.github/workflows/detect-non-english-issues.yml index ec569f63ca317d..d18726c8c793ba 100644 --- a/.github/workflows/detect-non-english-issues.yml +++ b/.github/workflows/detect-non-english-issues.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check issue language id: detect_language - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 env: ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} @@ -57,7 +57,7 @@ jobs: - name: Detect language using AI id: ai_language_detection if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1 + uses: actions/ai-inference@v2.0.1 with: model: openai/gpt-4o-mini system-prompt: | @@ -90,7 +90,7 @@ jobs: - name: Process non-English issues if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 env: AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }} ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index daaa737471370f..fb5deb2958f1bc 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -10,7 +10,7 @@ jobs: if: github.repository_owner == 'home-assistant' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 + - uses: dessant/lock-threads@v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: "30" diff --git a/.github/workflows/restrict-task-creation.yml b/.github/workflows/restrict-task-creation.yml index 1b78cae3e0fc14..beb14a80bed6a3 100644 --- a/.github/workflows/restrict-task-creation.yml +++ b/.github/workflows/restrict-task-creation.yml @@ -12,7 +12,7 @@ jobs: if: github.event.issue.type.name == 'Task' steps: - name: Check if user is authorized - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@v8 with: script: | const issueAuthor = context.payload.issue.user.login; diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 86be8cd4da5008..f0e2572fa54140 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues (-1) - name: 60 days stale PRs policy - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 60 @@ -57,7 +57,7 @@ jobs: # - No issues marked as no-stale or help-wanted # - No PRs (-1) - name: 90 days stale issues - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} days-before-stale: 90 @@ -87,7 +87,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} only-labels: "needs-more-information" diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index fb4cb43e7c0433..e0ffe2933e009a 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0292677ab9352f..7ac7c239816412 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -32,11 +32,11 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -91,7 +91,7 @@ jobs: ) > build_constraints.txt - name: Upload env_file - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: env_file path: ./.env_file @@ -99,14 +99,14 @@ jobs: overwrite: true - name: Upload build_constraints - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: build_constraints path: ./build_constraints.txt overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: requirements_diff path: ./requirements_diff.txt @@ -118,7 +118,7 @@ jobs: python -m script.gen_requirements_all ci - name: Upload requirements_all_wheels - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: requirements_all_wheels path: ./requirements_all_wheels_*.txt @@ -135,20 +135,20 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Download env_file - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: env_file - name: Download build_constraints - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: build_constraints - name: Download requirements_diff - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: requirements_diff @@ -159,7 +159,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 2025.07.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -184,25 +184,25 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@v5.0.0 - name: Download env_file - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: env_file - name: Download build_constraints - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: build_constraints - name: Download requirements_diff - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: requirements_diff - name: Download requirements_all_wheels - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@v5.0.0 with: name: requirements_all_wheels @@ -219,7 +219,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 2025.07.0 + uses: home-assistant/wheels@2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 From f1c55ee7e2b37f2d5c750099e9813677f4fe0a1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 06:04:58 -0500 Subject: [PATCH 02/17] Bump habluetooth to 5.6.4 (#152227) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ffffc3ec6f3e8f..431ec10b366d4f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==5.6.2" + "habluetooth==5.6.4" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98622eab1d2086..5b706e72f74288 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==5.6.2 +habluetooth==5.6.4 hass-nabucasa==1.1.1 hassil==3.2.0 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index a192b85e0c1d5b..49eadf63022867 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.5 # homeassistant.components.bluetooth -habluetooth==5.6.2 +habluetooth==5.6.4 # homeassistant.components.cloud hass-nabucasa==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b026a547cfc9b9..a31d995ca547de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.5 # homeassistant.components.bluetooth -habluetooth==5.6.2 +habluetooth==5.6.4 # homeassistant.components.cloud hass-nabucasa==1.1.1 From b87e581cde0e363e9624632b94eb69c4b9d8e5be Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Sep 2025 07:29:09 -0400 Subject: [PATCH 03/17] Drop use of aiofiles in TTS (#152208) --- homeassistant/components/tts/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index f1ffc7e0aada18..fcae7793185f63 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -18,7 +18,6 @@ from time import monotonic from typing import Any, Final, Generic, Protocol, TypeVar -import aiofiles from aiohttp import web import mutagen from mutagen.id3 import ID3, TextFrame as ID3Text @@ -591,13 +590,9 @@ async def _async_stream_override_result(self) -> AsyncGenerator[bytes]: if not needs_conversion: # Read file directly (no conversion) - async with aiofiles.open(self._override_media_path, "rb") as media_file: - while True: - chunk = await media_file.read(FFMPEG_CHUNK_SIZE) - if not chunk: - break - yield chunk - + yield await self.hass.async_add_executor_job( + self._override_media_path.read_bytes + ) return # Use ffmpeg to convert audio to preferred format From 3690497e1f519901b62f088d526d7b3ee69e97ab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:17:49 +0200 Subject: [PATCH 04/17] Update pydantic to 2.11.9 (#152213) --- homeassistant/package_constraints.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5b706e72f74288..91880ab74cbd46 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -129,7 +129,7 @@ multidict>=6.0.2 backoff>=2.0 # ensure pydantic version does not float since it might have breaking changes -pydantic==2.11.7 +pydantic==2.11.9 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 diff --git a/requirements_test.txt b/requirements_test.txt index 2d1057590e9c88..3e4b9b3d94cc0a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ license-expression==30.4.3 mock-open==1.4.0 mypy-dev==1.18.0a4 pre-commit==4.2.0 -pydantic==2.11.7 +pydantic==2.11.9 pylint==3.3.8 pylint-per-file-ignores==1.4.0 pipdeptree==2.26.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e482c01b3dd602..4efbcea9ab9848 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -155,7 +155,7 @@ backoff>=2.0 # ensure pydantic version does not float since it might have breaking changes -pydantic==2.11.7 +pydantic==2.11.9 # Required for Python 3.12.4 compatibility (#119223). mashumaro>=3.13.1 From b64d60fce45b953fab0ce94e0910ec9a69621512 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:30:17 +0200 Subject: [PATCH 05/17] Fix lg_thinq RuntimeWarning in tests (#152221) --- tests/components/lg_thinq/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/lg_thinq/conftest.py b/tests/components/lg_thinq/conftest.py index 73abc8c507537e..b830b0b44e4fc9 100644 --- a/tests/components/lg_thinq/conftest.py +++ b/tests/components/lg_thinq/conftest.py @@ -137,4 +137,5 @@ def devices(mock_thinq_api: AsyncMock, device_fixture: str) -> Generator[AsyncMo mock_thinq_api.async_get_device_status.return_value = load_json_object_fixture( f"{device_fixture}/status.json", DOMAIN ) + mock_thinq_api.async_get_device_energy_profile.return_value = MagicMock() return mock_thinq_api From 08485f4e09e368625ac8f4d98bb481ac8d7dadd3 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sat, 13 Sep 2025 10:36:15 -0400 Subject: [PATCH 06/17] Upgrade waterfurnace to 1.2.0 (#152241) --- homeassistant/components/waterfurnace/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 2bf72acb047b7b..98d21dd9425ec5 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_polling", "loggers": ["waterfurnace"], "quality_scale": "legacy", - "requirements": ["waterfurnace==1.1.0"] + "requirements": ["waterfurnace==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 49eadf63022867..7467f0fb736c62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3102,7 +3102,7 @@ wallbox==0.9.0 watchdog==6.0.0 # homeassistant.components.waterfurnace -waterfurnace==1.1.0 +waterfurnace==1.2.0 # homeassistant.components.watergate watergate-local-api==2024.4.1 From 97077898bbee6cce7e6401f1df85741fe808ead2 Mon Sep 17 00:00:00 2001 From: w531t4 <41222371+w531t4@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:39:11 -0400 Subject: [PATCH 07/17] Add Twitch entity for self (#150525) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/twitch/coordinator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/twitch/coordinator.py b/homeassistant/components/twitch/coordinator.py index 010a9e90cccc9e..142c3509e0b93b 100644 --- a/homeassistant/components/twitch/coordinator.py +++ b/homeassistant/components/twitch/coordinator.py @@ -79,6 +79,7 @@ async def _async_setup(self) -> None: if not (user := await first(self.twitch.get_users())): raise UpdateFailed("Logged in user not found") self.current_user = user + self.users.append(self.current_user) # Add current_user to users list. async def _async_update_data(self) -> dict[str, TwitchUpdate]: await self.session.async_ensure_token_valid() @@ -95,6 +96,8 @@ async def _async_update_data(self) -> dict[str, TwitchUpdate]: user_id=self.current_user.id, first=100 ) } + async for s in self.twitch.get_streams(user_id=[self.current_user.id]): + streams.update({s.user_id: s}) follows: dict[str, FollowedChannel] = { f.broadcaster_id: f async for f in await self.twitch.get_followed_channels( From 24c04cceee3ea89c2e1a4dfad519090c466cb74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Andr=C3=A9=20Roland?= Date: Sat, 13 Sep 2025 16:41:51 +0200 Subject: [PATCH 08/17] Reflect Verisure lock, alarm control panel and switch state immediately without cloud pull (#149479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Abílio Costa --- .../verisure/alarm_control_panel.py | 25 ++++++--- homeassistant/components/verisure/lock.py | 55 ++++++++++--------- homeassistant/components/verisure/switch.py | 2 +- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 7ead1f014c8181..db199b180f4958 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -67,8 +67,13 @@ async def _async_set_arm_state( ) LOGGER.debug("Verisure set arm state %s", state) result = None + attempts = 0 while result is None: - await asyncio.sleep(0.5) + if attempts == 30: + break + if attempts > 1: + await asyncio.sleep(0.5) + attempts += 1 transaction = await self.hass.async_add_executor_job( self.coordinator.verisure.request, self.coordinator.verisure.poll_arm_state( @@ -81,8 +86,10 @@ async def _async_set_arm_state( .get("armStateChangePollResult", {}) .get("result") ) - - await self.coordinator.async_refresh() + LOGGER.debug("Result is %s", result) + if result == "OK": + self._attr_alarm_state = ALARM_STATE_TO_HA.get(state) + self.async_write_ha_state() async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -108,16 +115,20 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None: "ARMED_AWAY", self.coordinator.verisure.arm_away(code) ) - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" + def _update_alarm_attributes(self) -> None: + """Update alarm state and changed by from coordinator data.""" self._attr_alarm_state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) self._attr_changed_by = self.coordinator.data["alarm"].get("name") + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_alarm_attributes() super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() - self._handle_coordinator_update() + self._update_alarm_attributes() diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 76aeedd05fa99b..4d2229967a09eb 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -10,7 +10,7 @@ from homeassistant.components.lock import LockEntity, LockState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, @@ -70,7 +70,9 @@ def __init__( self._attr_unique_id = serial_number self.serial_number = serial_number - self._state: str | None = None + self._attr_is_locked = None + self._attr_changed_by = None + self._changed_method: str | None = None @property def device_info(self) -> DeviceInfo: @@ -92,20 +94,6 @@ def available(self) -> bool: super().available and self.serial_number in self.coordinator.data["locks"] ) - @property - def changed_by(self) -> str | None: - """Last change triggered by.""" - return ( - self.coordinator.data["locks"][self.serial_number] - .get("user", {}) - .get("name") - ) - - @property - def changed_method(self) -> str: - """Last change method.""" - return self.coordinator.data["locks"][self.serial_number]["lockMethod"] - @property def code_format(self) -> str: """Return the configured code format.""" @@ -115,16 +103,9 @@ def code_format(self) -> str: return f"^\\d{{{digits}}}$" @property - def is_locked(self) -> bool: - """Return true if lock is locked.""" - return ( - self.coordinator.data["locks"][self.serial_number]["lockStatus"] == "LOCKED" - ) - - @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return the state attributes.""" - return {"method": self.changed_method} + return {"method": self._changed_method} async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -154,7 +135,7 @@ async def async_set_lock_state(self, code: str, state: LockState) -> None: target_state = "LOCKED" if state == LockState.LOCKED else "UNLOCKED" lock_status = None attempts = 0 - while lock_status != "OK": + while lock_status is None: if attempts == 30: break if attempts > 1: @@ -172,8 +153,10 @@ async def async_set_lock_state(self, code: str, state: LockState) -> None: .get("doorLockStateChangePollResult", {}) .get("result") ) + LOGGER.debug("Lock status is %s", lock_status) if lock_status == "OK": - self._state = state + self._attr_is_locked = state == LockState.LOCKED + self.async_write_ha_state() def disable_autolock(self) -> None: """Disable autolock on a doorlock.""" @@ -196,3 +179,21 @@ def enable_autolock(self) -> None: LOGGER.debug("Enabling autolock on %s", self.serial_number) except VerisureError as ex: LOGGER.error("Could not enable autolock, %s", ex) + + def _update_lock_attributes(self) -> None: + """Update lock state, changed by, and method from coordinator data.""" + lock_data = self.coordinator.data["locks"][self.serial_number] + self._attr_is_locked = lock_data["lockStatus"] == "LOCKED" + self._attr_changed_by = lock_data.get("user", {}).get("name") + self._changed_method = lock_data["lockMethod"] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_lock_attributes() + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._update_lock_attributes() diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 0deb1da5e95c62..bdd933c753b068 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -99,4 +99,4 @@ async def async_set_plug_state(self, state: bool) -> None: ) self._state = state self._change_timestamp = monotonic() - await self.coordinator.async_request_refresh() + self.async_write_ha_state() From be692ab2fd011c908b7c1c6aca6cb7bddba38317 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:47:33 +0200 Subject: [PATCH 09/17] Reapply "Pin SHA for all github actions" (#152233) --- .github/workflows/builder.yml | 44 ++--- .github/workflows/ci.yaml | 168 +++++++++--------- .github/workflows/codeql.yml | 6 +- .github/workflows/detect-duplicate-issues.yml | 8 +- .../workflows/detect-non-english-issues.yml | 6 +- .github/workflows/lock.yml | 2 +- .github/workflows/restrict-task-creation.yml | 2 +- .github/workflows/stale.yml | 6 +- .github/workflows/translations.yml | 4 +- .github/workflows/wheels.yml | 32 ++-- 10 files changed, 140 insertions(+), 138 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 63cafce6c731f4..81a327424feaea 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -27,12 +27,12 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: translations path: translations.tar.gz @@ -90,11 +90,11 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -105,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: OHF-Voice/intents-package @@ -116,7 +116,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -175,7 +175,7 @@ jobs: sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - name: Download translations - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: translations @@ -190,14 +190,14 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to GitHub Container Registry - uses: docker/login-action@v3.5.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2025.03.0 + uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0 with: args: | $BUILD_ARGS \ @@ -242,7 +242,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set build additional args run: | @@ -256,14 +256,14 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@v3.5.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2025.03.0 + uses: home-assistant/builder@71885366c80f6ead6ae8c364b61d910e0dc5addc # 2025.03.0 with: args: | $BUILD_ARGS \ @@ -279,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -321,23 +321,23 @@ jobs: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Cosign - uses: sigstore/cosign-installer@v3.9.2 + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 with: cosign-release: "v2.2.3" - name: Login to DockerHub if: matrix.registry == 'docker.io/homeassistant' - uses: docker/login-action@v3.5.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry if: matrix.registry == 'ghcr.io/home-assistant' - uses: docker/login-action@v3.5.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -454,15 +454,15 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: translations @@ -480,7 +480,7 @@ jobs: python -m build - name: Upload package to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d465f428a6219..41a2c1c7ea1ec6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: | @@ -120,7 +120,7 @@ jobs: run: | echo "key=$(lsb_release -rs)-apt-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}" >> $GITHUB_OUTPUT - name: Filter for core changes - uses: dorny/paths-filter@v3.0.2 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: core with: filters: .core_files.yaml @@ -135,7 +135,7 @@ jobs: echo "Result:" cat .integration_paths.yaml - name: Filter for integration changes - uses: dorny/paths-filter@v3.0.2 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: integrations with: filters: .integration_paths.yaml @@ -254,16 +254,16 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv key: >- @@ -279,7 +279,7 @@ jobs: uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v4.2.4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true @@ -300,16 +300,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -318,7 +318,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -340,16 +340,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -358,7 +358,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -380,16 +380,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -398,7 +398,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -470,7 +470,7 @@ jobs: - script/hassfest/docker/Dockerfile steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -489,10 +489,10 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -505,7 +505,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv key: >- @@ -513,7 +513,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v4.2.4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: ${{ env.UV_CACHE_DIR }} key: >- @@ -585,7 +585,7 @@ jobs: python --version uv pip freeze >> pip_freeze.txt - name: Upload pip_freeze artifact - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pip-freeze-${{ matrix.python-version }} path: pip_freeze.txt @@ -631,16 +631,16 @@ jobs: -o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \ libturbojpeg - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -664,16 +664,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -698,9 +698,9 @@ jobs: && github.event_name == 'pull_request' steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Dependency review - uses: actions/dependency-review-action@v4.7.3 + uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 with: license-check: false # We use our own license audit checks @@ -721,16 +721,16 @@ jobs: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -742,7 +742,7 @@ jobs: . venv/bin/activate python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json - name: Upload licenses - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: licenses-${{ github.run_number }}-${{ matrix.python-version }} path: licenses-${{ matrix.python-version }}.json @@ -764,16 +764,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -811,16 +811,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -856,10 +856,10 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true @@ -872,7 +872,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -880,7 +880,7 @@ jobs: ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache - uses: actions/cache@v4.2.4 + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: .mypy_cache key: >- @@ -947,16 +947,16 @@ jobs: libturbojpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -968,7 +968,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pytest_buckets path: pytest_buckets.txt @@ -1022,16 +1022,16 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1045,7 +1045,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Download pytest_buckets - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: pytest_buckets - name: Compile English translations @@ -1084,14 +1084,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1104,7 +1104,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml @@ -1169,16 +1169,16 @@ jobs: libmariadb-dev-compat \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1237,7 +1237,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1245,7 +1245,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1259,7 +1259,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-results-mariadb-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1325,16 +1325,16 @@ jobs: sudo apt-get -y install \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1394,7 +1394,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1402,7 +1402,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1416,7 +1416,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-results-postgres-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1437,14 +1437,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download all coverage artifacts - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: fail_ci_if_error: true flags: full-suite @@ -1498,16 +1498,16 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.4 + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1563,14 +1563,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -1583,7 +1583,7 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml @@ -1601,14 +1601,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download all coverage artifacts - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} @@ -1628,11 +1628,11 @@ jobs: timeout-minutes: 10 steps: - name: Download all coverage artifacts - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: pattern: test-results-* - name: Upload test results to Codecov - uses: codecov/test-results-action@v1 + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 with: fail_ci_if_error: true verbose: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 044aea8d2cff82..c3a5073d03898a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,14 +21,14 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.30.3 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.30.3 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: category: "/language:python" diff --git a/.github/workflows/detect-duplicate-issues.yml b/.github/workflows/detect-duplicate-issues.yml index 1997f1c02b0c63..801c4bb36bc95c 100644 --- a/.github/workflows/detect-duplicate-issues.yml +++ b/.github/workflows/detect-duplicate-issues.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check if integration label was added and extract details id: extract - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | // Debug: Log the event payload @@ -113,7 +113,7 @@ jobs: - name: Fetch similar issues id: fetch_similar if: steps.extract.outputs.should_continue == 'true' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }} CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }} @@ -231,7 +231,7 @@ jobs: - name: Detect duplicates using AI id: ai_detection if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/ai-inference@v2.0.1 + uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1 with: model: openai/gpt-4o system-prompt: | @@ -280,7 +280,7 @@ jobs: - name: Post duplicate detection results id: post_results if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: AI_RESPONSE: ${{ steps.ai_detection.outputs.response }} SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }} diff --git a/.github/workflows/detect-non-english-issues.yml b/.github/workflows/detect-non-english-issues.yml index d18726c8c793ba..ec569f63ca317d 100644 --- a/.github/workflows/detect-non-english-issues.yml +++ b/.github/workflows/detect-non-english-issues.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check issue language id: detect_language - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} @@ -57,7 +57,7 @@ jobs: - name: Detect language using AI id: ai_language_detection if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/ai-inference@v2.0.1 + uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1 with: model: openai/gpt-4o-mini system-prompt: | @@ -90,7 +90,7 @@ jobs: - name: Process non-English issues if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }} ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }} diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index fb5deb2958f1bc..daaa737471370f 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -10,7 +10,7 @@ jobs: if: github.repository_owner == 'home-assistant' runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5.0.1 + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: "30" diff --git a/.github/workflows/restrict-task-creation.yml b/.github/workflows/restrict-task-creation.yml index beb14a80bed6a3..1b78cae3e0fc14 100644 --- a/.github/workflows/restrict-task-creation.yml +++ b/.github/workflows/restrict-task-creation.yml @@ -12,7 +12,7 @@ jobs: if: github.event.issue.type.name == 'Task' steps: - name: Check if user is authorized - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const issueAuthor = context.payload.issue.user.login; diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f0e2572fa54140..86be8cd4da5008 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues (-1) - name: 60 days stale PRs policy - uses: actions/stale@v10.0.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 60 @@ -57,7 +57,7 @@ jobs: # - No issues marked as no-stale or help-wanted # - No PRs (-1) - name: 90 days stale issues - uses: actions/stale@v10.0.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} days-before-stale: 90 @@ -87,7 +87,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v10.0.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} only-labels: "needs-more-information" diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index e0ffe2933e009a..fb4cb43e7c0433 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v6.0.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7ac7c239816412..4aa9724f515216 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -32,11 +32,11 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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 }} check-latest: true @@ -91,7 +91,7 @@ jobs: ) > build_constraints.txt - name: Upload env_file - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: env_file path: ./.env_file @@ -99,14 +99,14 @@ jobs: overwrite: true - name: Upload build_constraints - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build_constraints path: ./build_constraints.txt overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: requirements_diff path: ./requirements_diff.txt @@ -118,7 +118,7 @@ jobs: python -m script.gen_requirements_all ci - name: Upload requirements_all_wheels - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: requirements_all_wheels path: ./requirements_all_wheels_*.txt @@ -135,20 +135,20 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download env_file - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: env_file - name: Download build_constraints - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: build_constraints - name: Download requirements_diff - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: requirements_diff @@ -158,6 +158,7 @@ jobs: sed -i "/uv/d" requirements.txt sed -i "/uv/d" requirements_diff.txt + # home-assistant/wheels doesn't support sha pinning - name: Build wheels uses: home-assistant/wheels@2025.07.0 with: @@ -184,25 +185,25 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v5.0.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Download env_file - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: env_file - name: Download build_constraints - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: build_constraints - name: Download requirements_diff - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: requirements_diff - name: Download requirements_all_wheels - uses: actions/download-artifact@v5.0.0 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: requirements_all_wheels @@ -218,6 +219,7 @@ jobs: sed -i "/uv/d" requirements.txt sed -i "/uv/d" requirements_diff.txt + # home-assistant/wheels doesn't support sha pinning - name: Build wheels uses: home-assistant/wheels@2025.07.0 with: From 82b57568a0af46ee0e7921024eafb4782716f99d Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Sat, 13 Sep 2025 12:38:47 -0400 Subject: [PATCH 10/17] Set diagnostic entity category for "mode" in APCUPSD (#152246) --- homeassistant/components/apcupsd/sensor.py | 1 + tests/components/apcupsd/snapshots/test_sensor.ambr | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 14baed5bfce715..00922b75ed8005 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -395,6 +395,7 @@ "upsmode": SensorEntityDescription( key="upsmode", translation_key="ups_mode", + entity_category=EntityCategory.DIAGNOSTIC, ), "upsname": SensorEntityDescription( key="upsname", diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr index 2e991d7cfa6be2..a873607180f887 100644 --- a/tests/components/apcupsd/snapshots/test_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -868,7 +868,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.myups_mode', 'has_entity_name': True, 'hidden_by': None, From e9fbe2227f45f17e367298c1369694202d997100 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 12:24:05 -0500 Subject: [PATCH 11/17] Fix HomeKit Controller overwhelming resource-limited devices by batching characteristic polling (#152209) --- .../homekit_controller/connection.py | 55 +++++++---- .../homekit_controller/test_connection.py | 96 ++++++++++++++++++- 2 files changed, 127 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index ce8dc498d6d512..e20842d186f95f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -57,7 +57,10 @@ RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 - +# HomeKit accessories have varying limits on how many characteristics +# they can handle per request. Since we don't know each device's specific limit, +# we batch requests to a conservative size to avoid overwhelming any device. +MAX_CHARACTERISTICS_PER_REQUEST = 49 BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds @@ -326,16 +329,20 @@ async def async_setup(self) -> None: ) entry.async_on_unload(self._async_cancel_subscription_timer) + if transport != Transport.BLE: + # Although async_populate_accessories_state fetched the accessory database, + # the /accessories endpoint may return cached values from the accessory's + # perspective. For example, Ecobee thermostats may report stale temperature + # values (like 100°C) in their /accessories response after restarting. + # We need to explicitly poll characteristics to get fresh sensor readings + # before processing the entity map and creating devices. + # Use poll_all=True since entities haven't registered their characteristics yet. + await self.async_update(poll_all=True) + await self.async_process_entity_map() if transport != Transport.BLE: - # When Home Assistant starts, we restore the accessory map from storage - # which contains characteristic values from when HA was last running. - # These values are stale and may be incorrect (e.g., Ecobee thermostats - # report 100°C when restarting). We need to poll for fresh values before - # creating entities. Use poll_all=True since entities haven't registered - # their characteristics yet. - await self.async_update(poll_all=True) + # Start regular polling after entity map is processed self._async_start_polling() # If everything is up to date, we can create the entities @@ -938,20 +945,26 @@ async def async_update( async with self._polling_lock: _LOGGER.debug("Starting HomeKit device update: %s", self.unique_id) - try: - new_values_dict = await self.get_characteristics(to_poll) - except AccessoryNotFoundError: - # Not only did the connection fail, but also the accessory is not - # visible on the network. - self.async_set_available_state(False) - return - except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device may still available but our - # connection was dropped or we are reconnecting - self._poll_failures += 1 - if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE: + new_values_dict: dict[tuple[int, int], dict[str, Any]] = {} + to_poll_list = list(to_poll) + + for i in range(0, len(to_poll_list), MAX_CHARACTERISTICS_PER_REQUEST): + batch = to_poll_list[i : i + MAX_CHARACTERISTICS_PER_REQUEST] + try: + batch_values = await self.get_characteristics(batch) + new_values_dict.update(batch_values) + except AccessoryNotFoundError: + # Not only did the connection fail, but also the accessory is not + # visible on the network. self.async_set_available_state(False) - return + return + except (AccessoryDisconnectedError, EncryptionError): + # Temporary connection failure. Device may still available but our + # connection was dropped or we are reconnecting + self._poll_failures += 1 + if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE: + self.async_set_available_state(False) + return self._poll_failures = 0 self.process_new_events(new_values_dict) diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 99203d400fea5b..6c5ccdfd8b0a90 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -13,6 +13,9 @@ import pytest from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE +from homeassistant.components.homekit_controller.connection import ( + MAX_CHARACTERISTICS_PER_REQUEST, +) from homeassistant.components.homekit_controller.const import ( DEBOUNCE_COOLDOWN, DOMAIN, @@ -377,9 +380,15 @@ def _create_accessory(accessory: Accessory) -> Service: state = await helper.poll_and_get_state() assert state.state == STATE_OFF assert mock_get_characteristics.call_count == 2 - # Verify everything is polled - assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 10), (1, 11)} - assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 10), (1, 11)} + # Verify everything is polled (convert to set for comparison since batching changes the type) + assert set(mock_get_characteristics.call_args_list[0][0][0]) == { + (1, 10), + (1, 11), + } + assert set(mock_get_characteristics.call_args_list[1][0][0]) == { + (1, 10), + (1, 11), + } # Test device goes offline helper.pairing.available = False @@ -526,3 +535,84 @@ async def mock_get_characteristics( state = hass.states.get("climate.homew") assert state is not None assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + +async def test_characteristic_polling_batching( + hass: HomeAssistant, get_next_aid: Callable[[], int] +) -> None: + """Test that characteristic polling is batched to MAX_CHARACTERISTICS_PER_REQUEST.""" + + # Create a large accessory with many characteristics (more than 49) + def create_large_accessory_with_many_chars(accessory: Accessory) -> None: + """Create an accessory with many characteristics to test batching.""" + # Add multiple services with many characteristics each + for service_num in range(10): # 10 services + service = accessory.add_service( + ServicesTypes.LIGHTBULB, name=f"Light {service_num}" + ) + # Each lightbulb service gets several characteristics + service.add_char(CharacteristicsTypes.ON) + service.add_char(CharacteristicsTypes.BRIGHTNESS) + service.add_char(CharacteristicsTypes.HUE) + service.add_char(CharacteristicsTypes.SATURATION) + service.add_char(CharacteristicsTypes.COLOR_TEMPERATURE) + # Set initial values + for char in service.characteristics: + if char.type != CharacteristicsTypes.IDENTIFY: + char.value = 0 + + helper = await setup_test_component( + hass, get_next_aid(), create_large_accessory_with_many_chars + ) + + # Track the get_characteristics calls + get_chars_calls = [] + original_get_chars = helper.pairing.get_characteristics + + async def mock_get_characteristics(chars): + """Mock get_characteristics to track batch sizes.""" + get_chars_calls.append(list(chars)) + return await original_get_chars(chars) + + # Clear any calls from setup + get_chars_calls.clear() + + # Patch get_characteristics to track calls + with mock.patch.object( + helper.pairing, "get_characteristics", side_effect=mock_get_characteristics + ): + # Trigger an update through time_changed which simulates regular polling + # time_changed expects seconds, not a datetime + await time_changed(hass, 300) # 5 minutes in seconds + await hass.async_block_till_done() + + # We created 10 lightbulb services with 5 characteristics each = 50 total + # Plus any base accessory characteristics that are pollable + # This should result in exactly 2 batches + assert len(get_chars_calls) == 2, ( + f"Should have made exactly 2 batched calls, got {len(get_chars_calls)}" + ) + + # Check that no batch exceeded MAX_CHARACTERISTICS_PER_REQUEST + for i, batch in enumerate(get_chars_calls): + assert len(batch) <= MAX_CHARACTERISTICS_PER_REQUEST, ( + f"Batch {i} size {len(batch)} exceeded maximum {MAX_CHARACTERISTICS_PER_REQUEST}" + ) + + # Verify the total number of characteristics polled + total_chars = sum(len(batch) for batch in get_chars_calls) + # Each lightbulb has: ON, BRIGHTNESS, HUE, SATURATION, COLOR_TEMPERATURE = 5 + # 10 lightbulbs = 50 characteristics + assert total_chars == 50, ( + f"Should have polled exactly 50 characteristics, got {total_chars}" + ) + + # The first batch should be full (49 characteristics) + assert len(get_chars_calls[0]) == 49, ( + f"First batch should have exactly 49 characteristics, got {len(get_chars_calls[0])}" + ) + + # The second batch should have exactly 1 characteristic + assert len(get_chars_calls[1]) == 1, ( + f"Second batch should have exactly 1 characteristic, got {len(get_chars_calls[1])}" + ) From 5a5b639aa4e7f85b0cd59c064c985a7f7d55bc57 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:27:47 +0200 Subject: [PATCH 12/17] Update pytest-asyncio to 1.2.0 (#152156) --- pyproject.toml | 1 + requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 007cda7fad47c1..c81dd7e00f3cbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -448,6 +448,7 @@ testpaths = ["tests"] norecursedirs = [".git", "testing_config"] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_debug = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ diff --git a/requirements_test.txt b/requirements_test.txt index 3e4b9b3d94cc0a..81a1c152435573 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -19,7 +19,7 @@ pydantic==2.11.9 pylint==3.3.8 pylint-per-file-ignores==1.4.0 pipdeptree==2.26.1 -pytest-asyncio==1.1.0 +pytest-asyncio==1.2.0 pytest-aiohttp==1.1.0 pytest-cov==7.0.0 pytest-freezer==0.4.9 From df1302fc1c1a8e43155b1bf720b5b7aaceee06c9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:28:02 +0200 Subject: [PATCH 13/17] Update mypy-dev to 1.19.0a2 (#152250) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 81a1c152435573..658c3ab0a7a1b1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.5.2 go2rtc-client==0.2.1 license-expression==30.4.3 mock-open==1.4.0 -mypy-dev==1.18.0a4 +mypy-dev==1.19.0a2 pre-commit==4.2.0 pydantic==2.11.9 pylint==3.3.8 From 96034e15250656edacb385d5ed076fe95ea9ca7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 12:44:34 -0500 Subject: [PATCH 14/17] Bump aiohomekit to 3.2.16 (#152255) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index d15479aa9d5b3c..ef4fdadb24c41f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.15"], + "requirements": ["aiohomekit==3.2.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7467f0fb736c62..5e409a7cf6e7ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -271,7 +271,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.15 +aiohomekit==3.2.16 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a31d995ca547de..1752ff171fb0cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,7 +256,7 @@ aiohasupervisor==0.3.2 aiohomeconnect==0.19.0 # homeassistant.components.homekit_controller -aiohomekit==3.2.15 +aiohomekit==3.2.16 # homeassistant.components.mcp_server aiohttp_sse==2.2.0 From 4c267187390af806b00855285b0b958a701cb594 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Sep 2025 13:19:37 -0500 Subject: [PATCH 15/17] Bump bluetooth-auto-recovery to 1.5.3 (#152256) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 431ec10b366d4f..bf5345e0ba476a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak==1.0.1", "bleak-retry-connector==4.4.3", "bluetooth-adapters==2.1.0", - "bluetooth-auto-recovery==1.5.2", + "bluetooth-auto-recovery==1.5.3", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", "habluetooth==5.6.4" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 91880ab74cbd46..b6c5e88984d5e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bcrypt==4.3.0 bleak-retry-connector==4.4.3 bleak==1.0.1 bluetooth-adapters==2.1.0 -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 bluetooth-data-tools==1.28.2 cached-ipaddress==0.10.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 5e409a7cf6e7ef..dbcd081aed7f65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1752ff171fb0cd..12386cc708fb08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==2.1.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.2 +bluetooth-auto-recovery==1.5.3 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From 0e2c2ad355a67113b0f2fb52f790ffb82d22a0b9 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sat, 13 Sep 2025 22:34:12 +0300 Subject: [PATCH 16/17] Create dir on media upload if not exists (#152254) --- .../components/media_source/local_source.py | 9 ++++----- .../media_source/test_local_source.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 5a279753507978..bbfa288d59533b 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -133,14 +133,13 @@ async def async_upload_media( def _do_move() -> None: """Move file to target.""" - if not target_dir.is_dir(): - raise PathNotSupportedError("Target is not an existing directory") - - target_path = target_dir / uploaded_file.filename - try: + target_path = target_dir / uploaded_file.filename + target_path.relative_to(target_dir) raise_if_invalid_path(str(target_path)) + + target_dir.mkdir(parents=True, exist_ok=True) except ValueError as err: raise PathNotSupportedError("Invalid path") from err diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index d40dd7475a771d..d897c6216ae360 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -164,19 +164,21 @@ def get_file(name): client = await hass_client() # Test normal upload - res = await client.post( - "/api/media_source/local_source/upload", - data={ - "media_content_id": "media-source://media_source/test_dir", - "file": get_file("logo.png"), - }, - ) + with patch.object(Path, "mkdir", autospec=True, return_value=None) as mock_mkdir: + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir", + "file": get_file("logo.png"), + }, + ) assert res.status == 200 data = await res.json() assert data["media_content_id"] == "media-source://media_source/test_dir/logo.png" uploaded_path = Path(temp_dir) / "logo.png" assert uploaded_path.is_file() + mock_mkdir.assert_called_once() resolved = await media_source.async_resolve_media( hass, data["media_content_id"], target_media_player=None @@ -187,8 +189,6 @@ def get_file(name): # Test with bad media source ID for bad_id in ( - # Subdir doesn't exist - "media-source://media_source/test_dir/some-other-dir", # Main dir doesn't exist "media-source://media_source/test_dir2", # Location is invalid From 70df7b850335c8f8fc346e7f32157ea7428c01bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 13 Sep 2025 22:21:29 +0200 Subject: [PATCH 17/17] Restructure template engine, add crypto & base64 Jinja extension (#152261) --- .../{template.py => template/__init__.py} | 194 +++++++----------- .../helpers/template/extensions/__init__.py | 6 + .../helpers/template/extensions/base.py | 60 ++++++ .../helpers/template/extensions/base64.py | 50 +++++ .../helpers/template/extensions/crypto.py | 64 ++++++ tests/helpers/template/__init__.py | 1 + tests/helpers/template/extensions/__init__.py | 1 + .../template/extensions/test_base64.py | 43 ++++ .../template/extensions/test_crypto.py | 58 ++++++ .../snapshots/test_init.ambr} | 0 .../test_init.py} | 87 -------- 11 files changed, 357 insertions(+), 207 deletions(-) rename homeassistant/helpers/{template.py => template/__init__.py} (95%) create mode 100644 homeassistant/helpers/template/extensions/__init__.py create mode 100644 homeassistant/helpers/template/extensions/base.py create mode 100644 homeassistant/helpers/template/extensions/base64.py create mode 100644 homeassistant/helpers/template/extensions/crypto.py create mode 100644 tests/helpers/template/__init__.py create mode 100644 tests/helpers/template/extensions/__init__.py create mode 100644 tests/helpers/template/extensions/test_base64.py create mode 100644 tests/helpers/template/extensions/test_crypto.py rename tests/helpers/{snapshots/test_template.ambr => template/snapshots/test_init.ambr} (100%) rename tests/helpers/{test_template.py => template/test_init.py} (98%) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template/__init__.py similarity index 95% rename from homeassistant/helpers/template.py rename to homeassistant/helpers/template/__init__.py index 8e3106093aaf09..357a16c73404ea 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template/__init__.py @@ -4,7 +4,6 @@ from ast import literal_eval import asyncio -import base64 import collections.abc from collections.abc import Callable, Generator, Iterable, MutableSequence from contextlib import AbstractContextManager @@ -12,7 +11,6 @@ from copy import deepcopy from datetime import date, datetime, time, timedelta from functools import cache, lru_cache, partial, wraps -import hashlib import json import logging import math @@ -71,6 +69,19 @@ valid_entity_id, ) from homeassistant.exceptions import TemplateError +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, + floor_registry as fr, + issue_registry as ir, + label_registry as lr, + location as loc_helper, +) +from homeassistant.helpers.deprecation import deprecated_function +from homeassistant.helpers.singleton import singleton +from homeassistant.helpers.translation import async_translate_state +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import ( convert, @@ -84,20 +95,6 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.thread import ThreadWithException -from . import ( - area_registry, - device_registry, - entity_registry, - floor_registry as fr, - issue_registry, - label_registry, - location as loc_helper, -) -from .deprecation import deprecated_function -from .singleton import singleton -from .translation import async_translate_state -from .typing import TemplateVarsType - if TYPE_CHECKING: from _typeshed import OptExcInfo @@ -210,7 +207,7 @@ def _async_adjust_lru_sizes(_: Any) -> None: if new_size > current_size: lru.set_size(new_size) - from .event import async_track_time_interval # noqa: PLC0415 + from homeassistant.helpers.event import async_track_time_interval # noqa: PLC0415 cancel = async_track_time_interval( hass, _async_adjust_lru_sizes, timedelta(minutes=10) @@ -525,7 +522,10 @@ def __init__(self, template: str, hass: HomeAssistant | None = None) -> None: Note: A valid hass instance should always be passed in. The hass parameter will be non optional in Home Assistant Core 2025.10. """ - from .frame import ReportBehavior, report_usage # noqa: PLC0415 + from homeassistant.helpers.frame import ( # noqa: PLC0415 + ReportBehavior, + report_usage, + ) if not isinstance(template, str): raise TypeError("Expected template to be a string") @@ -973,7 +973,7 @@ def __call__(self, entity_id: str) -> str | None: state_value = state.state domain = state.domain device_class = state.attributes.get("device_class") - entry = entity_registry.async_get(self._hass).async_get(entity_id) + entry = er.async_get(self._hass).async_get(entity_id) platform = None if entry is None else entry.platform translation_key = None if entry is None else entry.translation_key @@ -1274,7 +1274,7 @@ def forgiving_boolean[_T]( """Try to convert value to a boolean.""" try: # Import here, not at top-level to avoid circular import - from . import config_validation as cv # noqa: PLC0415 + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 return cv.boolean(value) except vol.Invalid: @@ -1299,7 +1299,7 @@ def result_as_boolean(template_result: Any | None) -> bool: def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: """Expand out any groups and zones into entity states.""" # circular import. - from . import entity as entity_helper # noqa: PLC0415 + from homeassistant.helpers import entity as entity_helper # noqa: PLC0415 search = list(args) found = {} @@ -1341,8 +1341,8 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: """Get entity ids for entities tied to a device.""" - entity_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_device(entity_reg, _device_id) + entity_reg = er.async_get(hass) + entries = er.async_entries_for_device(entity_reg, _device_id) return [entry.entity_id for entry in entries] @@ -1360,19 +1360,17 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: # first try if there are any config entries with a matching title entities: list[str] = [] - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) for entry in hass.config_entries.async_entries(): if entry.title != entry_name: continue - entries = entity_registry.async_entries_for_config_entry( - ent_reg, entry.entry_id - ) + entries = er.async_entries_for_config_entry(ent_reg, entry.entry_id) entities.extend(entry.entity_id for entry in entries) if entities: return entities # fallback to just returning all entities for a domain - from .entity import entity_sources # noqa: PLC0415 + from homeassistant.helpers.entity import entity_sources # noqa: PLC0415 return [ entity_id @@ -1383,7 +1381,7 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: def config_entry_id(hass: HomeAssistant, entity_id: str) -> str | None: """Get an config entry ID from an entity ID.""" - entity_reg = entity_registry.async_get(hass) + entity_reg = er.async_get(hass) if entity := entity_reg.async_get(entity_id): return entity.config_entry_id return None @@ -1391,12 +1389,12 @@ def config_entry_id(hass: HomeAssistant, entity_id: str) -> str | None: def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: """Get a device ID from an entity ID or device name.""" - entity_reg = entity_registry.async_get(hass) + entity_reg = er.async_get(hass) entity = entity_reg.async_get(entity_id_or_device_name) if entity is not None: return entity.device_id - dev_reg = device_registry.async_get(hass) + dev_reg = dr.async_get(hass) return next( ( device_id @@ -1410,13 +1408,13 @@ def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: def device_name(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the device name from an device id, or entity id.""" - device_reg = device_registry.async_get(hass) + device_reg = dr.async_get(hass) if device := device_reg.async_get(lookup_value): return device.name_by_user or device.name - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # noqa: PLC0415 + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1432,7 +1430,7 @@ def device_name(hass: HomeAssistant, lookup_value: str) -> str | None: def device_attr(hass: HomeAssistant, device_or_entity_id: str, attr_name: str) -> Any: """Get the device specific attribute.""" - device_reg = device_registry.async_get(hass) + device_reg = dr.async_get(hass) if not isinstance(device_or_entity_id, str): raise TemplateError("Must provide a device or entity ID") device = None @@ -1475,14 +1473,14 @@ def is_device_attr( def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]: """Return all open issues.""" - current_issues = issue_registry.async_get(hass).issues + current_issues = ir.async_get(hass).issues # Use JSON for safe representation return {k: v.to_json() for (k, v) in current_issues.items()} def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | None: """Get issue by domain and issue_id.""" - result = issue_registry.async_get(hass).async_get_issue(domain, issue_id) + result = ir.async_get(hass).async_get_issue(domain, issue_id) if result: return result.to_json() return None @@ -1505,7 +1503,7 @@ def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None: return floors_list[0].floor_id if aid := area_id(hass, lookup_value): - area_reg = area_registry.async_get(hass) + area_reg = ar.async_get(hass) if area := area_reg.async_get_area(aid): return area.floor_id @@ -1519,7 +1517,7 @@ def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None: return floor.name if aid := area_id(hass, lookup_value): - area_reg = area_registry.async_get(hass) + area_reg = ar.async_get(hass) if ( (area := area_reg.async_get_area(aid)) and area.floor_id @@ -1542,8 +1540,8 @@ def floor_areas(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: if _floor_id is None: return [] - area_reg = area_registry.async_get(hass) - entries = area_registry.async_entries_for_floor(area_reg, _floor_id) + area_reg = ar.async_get(hass) + entries = ar.async_entries_for_floor(area_reg, _floor_id) return [entry.id for entry in entries if entry.id] @@ -1558,12 +1556,12 @@ def floor_entities(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: def areas(hass: HomeAssistant) -> Iterable[str | None]: """Return all areas.""" - return list(area_registry.async_get(hass).areas) + return list(ar.async_get(hass).areas) def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the area ID from an area name, alias, device id, or entity id.""" - area_reg = area_registry.async_get(hass) + area_reg = ar.async_get(hass) lookup_str = str(lookup_value) if area := area_reg.async_get_area_by_name(lookup_str): return area.id @@ -1571,10 +1569,10 @@ def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: if areas_list: return areas_list[0].id - ent_reg = entity_registry.async_get(hass) - dev_reg = device_registry.async_get(hass) + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # noqa: PLC0415 + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1596,7 +1594,7 @@ def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: return None -def _get_area_name(area_reg: area_registry.AreaRegistry, valid_area_id: str) -> str: +def _get_area_name(area_reg: ar.AreaRegistry, valid_area_id: str) -> str: """Get area name from valid area ID.""" area = area_reg.async_get_area(valid_area_id) assert area @@ -1605,14 +1603,14 @@ def _get_area_name(area_reg: area_registry.AreaRegistry, valid_area_id: str) -> def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the area name from an area id, device id, or entity id.""" - area_reg = area_registry.async_get(hass) + area_reg = ar.async_get(hass) if area := area_reg.async_get_area(lookup_value): return area.name - dev_reg = device_registry.async_get(hass) - ent_reg = entity_registry.async_get(hass) + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # noqa: PLC0415 + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1649,19 +1647,18 @@ def area_entities(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: _area_id = area_id_or_name if _area_id is None: return [] - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) entity_ids = [ - entry.entity_id - for entry in entity_registry.async_entries_for_area(ent_reg, _area_id) + entry.entity_id for entry in er.async_entries_for_area(ent_reg, _area_id) ] - dev_reg = device_registry.async_get(hass) + dev_reg = dr.async_get(hass) # We also need to add entities tied to a device in the area that don't themselves # have an area specified since they inherit the area from the device. entity_ids.extend( [ entity.entity_id - for device in device_registry.async_entries_for_area(dev_reg, _area_id) - for entity in entity_registry.async_entries_for_device(ent_reg, device.id) + for device in dr.async_entries_for_area(dev_reg, _area_id) + for entity in er.async_entries_for_device(ent_reg, device.id) if entity.area_id is None ] ) @@ -1679,21 +1676,21 @@ def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: _area_id = area_id(hass, area_id_or_name) if _area_id is None: return [] - dev_reg = device_registry.async_get(hass) - entries = device_registry.async_entries_for_area(dev_reg, _area_id) + dev_reg = dr.async_get(hass) + entries = dr.async_entries_for_area(dev_reg, _area_id) return [entry.id for entry in entries] def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None]: """Return all labels, or those from a area ID, device ID, or entity ID.""" - label_reg = label_registry.async_get(hass) + label_reg = lr.async_get(hass) if lookup_value is None: return list(label_reg.labels) - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # noqa: PLC0415 + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 lookup_value = str(lookup_value) @@ -1706,12 +1703,12 @@ def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None return list(entity.labels) # Check if this could be a device ID - dev_reg = device_registry.async_get(hass) + dev_reg = dr.async_get(hass) if device := dev_reg.async_get(lookup_value): return list(device.labels) # Check if this could be a area ID - area_reg = area_registry.async_get(hass) + area_reg = ar.async_get(hass) if area := area_reg.async_get_area(lookup_value): return list(area.labels) @@ -1720,7 +1717,7 @@ def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None def label_id(hass: HomeAssistant, lookup_value: Any) -> str | None: """Get the label ID from a label name.""" - label_reg = label_registry.async_get(hass) + label_reg = lr.async_get(hass) if label := label_reg.async_get_label_by_name(str(lookup_value)): return label.label_id return None @@ -1728,7 +1725,7 @@ def label_id(hass: HomeAssistant, lookup_value: Any) -> str | None: def label_name(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the label name from a label ID.""" - label_reg = label_registry.async_get(hass) + label_reg = lr.async_get(hass) if label := label_reg.async_get_label(lookup_value): return label.name return None @@ -1736,7 +1733,7 @@ def label_name(hass: HomeAssistant, lookup_value: str) -> str | None: def label_description(hass: HomeAssistant, lookup_value: str) -> str | None: """Get the label description from a label ID.""" - label_reg = label_registry.async_get(hass) + label_reg = lr.async_get(hass) if label := label_reg.async_get_label(lookup_value): return label.description return None @@ -1755,8 +1752,8 @@ def label_areas(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: """Return areas for a given label ID or name.""" if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: return [] - area_reg = area_registry.async_get(hass) - entries = area_registry.async_entries_for_label(area_reg, _label_id) + area_reg = ar.async_get(hass) + entries = ar.async_entries_for_label(area_reg, _label_id) return [entry.id for entry in entries] @@ -1764,8 +1761,8 @@ def label_devices(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: """Return device IDs for a given label ID or name.""" if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: return [] - dev_reg = device_registry.async_get(hass) - entries = device_registry.async_entries_for_label(dev_reg, _label_id) + dev_reg = dr.async_get(hass) + entries = dr.async_entries_for_label(dev_reg, _label_id) return [entry.id for entry in entries] @@ -1773,8 +1770,8 @@ def label_entities(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: """Return entities for a given label ID or name.""" if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: return [] - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_label(ent_reg, _label_id) + ent_reg = er.async_get(hass) + entries = er.async_entries_for_label(ent_reg, _label_id) return [entry.entity_id for entry in entries] @@ -1913,7 +1910,7 @@ def distance(hass: HomeAssistant, *args: Any) -> float | None: def is_hidden_entity(hass: HomeAssistant, entity_id: str) -> bool: """Test if an entity is hidden.""" - entity_reg = entity_registry.async_get(hass) + entity_reg = er.async_get(hass) entry = entity_reg.async_get(entity_id) return entry is not None and entry.hidden @@ -2608,22 +2605,6 @@ def from_hex(value: str) -> bytes: return bytes.fromhex(value) -def base64_encode(value: str | bytes) -> str: - """Perform base64 encode.""" - if isinstance(value, str): - value = value.encode("utf-8") - return base64.b64encode(value).decode("utf-8") - - -def base64_decode(value: str, encoding: str | None = "utf-8") -> str | bytes: - """Perform base64 decode.""" - decoded = base64.b64decode(value) - if encoding: - return decoded.decode(encoding) - - return decoded - - def ordinal(value): """Perform ordinal conversion.""" suffixes = ["th", "st", "nd", "rd"] + ["th"] * 6 # codespell:ignore nd @@ -2928,26 +2909,6 @@ def combine(*args: Any, recursive: bool = False) -> dict[Any, Any]: return result -def md5(value: str) -> str: - """Generate md5 hash from a string.""" - return hashlib.md5(value.encode()).hexdigest() - - -def sha1(value: str) -> str: - """Generate sha1 hash from a string.""" - return hashlib.sha1(value.encode()).hexdigest() - - -def sha256(value: str) -> str: - """Generate sha256 hash from a string.""" - return hashlib.sha256(value.encode()).hexdigest() - - -def sha512(value: str) -> str: - """Generate sha512 hash from a string.""" - return hashlib.sha512(value.encode()).hexdigest() - - class TemplateContextManager(AbstractContextManager): """Context manager to store template being parsed or rendered in a ContextVar.""" @@ -3096,11 +3057,14 @@ def __init__( """Initialise template environment.""" super().__init__(undefined=make_logging_undefined(strict, log_fn)) self.hass = hass + self.limited = limited self.template_cache: weakref.WeakValueDictionary[ str | jinja2.nodes.Template, CodeType | None ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.loopcontrols") self.add_extension("jinja2.ext.do") + self.add_extension("homeassistant.helpers.template.extensions.Base64Extension") + self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension") self.globals["acos"] = arc_cosine self.globals["as_datetime"] = as_datetime @@ -3125,16 +3089,12 @@ def __init__( self.globals["is_number"] = is_number self.globals["log"] = logarithm self.globals["max"] = min_max_from_filter(self.filters["max"], "max") - self.globals["md5"] = md5 self.globals["median"] = median self.globals["merge_response"] = merge_response self.globals["min"] = min_max_from_filter(self.filters["min"], "min") self.globals["pack"] = struct_pack self.globals["pi"] = math.pi self.globals["set"] = _to_set - self.globals["sha1"] = sha1 - self.globals["sha256"] = sha256 - self.globals["sha512"] = sha512 self.globals["shuffle"] = shuffle self.globals["sin"] = sine self.globals["slugify"] = slugify @@ -3165,8 +3125,6 @@ def __init__( self.filters["atan"] = arc_tangent self.filters["atan2"] = arc_tangent2 self.filters["average"] = average - self.filters["base64_decode"] = base64_decode - self.filters["base64_encode"] = base64_encode self.filters["bitwise_and"] = bitwise_and self.filters["bitwise_or"] = bitwise_or self.filters["bitwise_xor"] = bitwise_xor @@ -3185,7 +3143,6 @@ def __init__( self.filters["is_defined"] = fail_when_undefined self.filters["is_number"] = is_number self.filters["log"] = logarithm - self.filters["md5"] = md5 self.filters["median"] = median self.filters["multiply"] = multiply self.filters["ord"] = ord @@ -3198,9 +3155,6 @@ def __init__( self.filters["regex_replace"] = regex_replace self.filters["regex_search"] = regex_search self.filters["round"] = forgiving_round - self.filters["sha1"] = sha1 - self.filters["sha256"] = sha256 - self.filters["sha512"] = sha512 self.filters["shuffle"] = shuffle self.filters["sin"] = sine self.filters["slugify"] = slugify diff --git a/homeassistant/helpers/template/extensions/__init__.py b/homeassistant/helpers/template/extensions/__init__.py new file mode 100644 index 00000000000000..d1ed7e093faf99 --- /dev/null +++ b/homeassistant/helpers/template/extensions/__init__.py @@ -0,0 +1,6 @@ +"""Home Assistant template extensions.""" + +from .base64 import Base64Extension +from .crypto import CryptoExtension + +__all__ = ["Base64Extension", "CryptoExtension"] diff --git a/homeassistant/helpers/template/extensions/base.py b/homeassistant/helpers/template/extensions/base.py new file mode 100644 index 00000000000000..142e9e77d5ecf4 --- /dev/null +++ b/homeassistant/helpers/template/extensions/base.py @@ -0,0 +1,60 @@ +"""Base extension class for Home Assistant template extensions.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from jinja2.ext import Extension +from jinja2.nodes import Node +from jinja2.parser import Parser + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +@dataclass +class TemplateFunction: + """Definition for a template function, filter, or test.""" + + name: str + func: Callable[..., Any] + as_global: bool = False + as_filter: bool = False + as_test: bool = False + limited_ok: bool = ( + True # Whether this function is available in limited environments + ) + + +class BaseTemplateExtension(Extension): + """Base class for Home Assistant template extensions.""" + + environment: TemplateEnvironment + + def __init__( + self, + environment: TemplateEnvironment, + *, + functions: list[TemplateFunction] | None = None, + ) -> None: + """Initialize the extension with a list of template functions.""" + super().__init__(environment) + + if functions: + for template_func in functions: + # Skip functions not allowed in limited environments + if self.environment.limited and not template_func.limited_ok: + continue + + if template_func.as_global: + environment.globals[template_func.name] = template_func.func + if template_func.as_filter: + environment.filters[template_func.name] = template_func.func + if template_func.as_test: + environment.tests[template_func.name] = template_func.func + + def parse(self, parser: Parser) -> Node | list[Node]: + """Required by Jinja2 Extension base class.""" + return [] diff --git a/homeassistant/helpers/template/extensions/base64.py b/homeassistant/helpers/template/extensions/base64.py new file mode 100644 index 00000000000000..3ec88bf14f4794 --- /dev/null +++ b/homeassistant/helpers/template/extensions/base64.py @@ -0,0 +1,50 @@ +"""Base64 encoding and decoding functions for Home Assistant templates.""" + +from __future__ import annotations + +import base64 +from typing import TYPE_CHECKING + +from .base import BaseTemplateExtension, TemplateFunction + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +class Base64Extension(BaseTemplateExtension): + """Jinja2 extension for base64 encoding and decoding functions.""" + + def __init__(self, environment: TemplateEnvironment) -> None: + """Initialize the base64 extension.""" + super().__init__( + environment, + functions=[ + TemplateFunction( + "base64_encode", + self.base64_encode, + as_filter=True, + limited_ok=False, + ), + TemplateFunction( + "base64_decode", + self.base64_decode, + as_filter=True, + limited_ok=False, + ), + ], + ) + + @staticmethod + def base64_encode(value: str | bytes) -> str: + """Encode a string or bytes to base64.""" + if isinstance(value, str): + value = value.encode("utf-8") + return base64.b64encode(value).decode("utf-8") + + @staticmethod + def base64_decode(value: str, encoding: str | None = "utf-8") -> str | bytes: + """Decode a base64 string.""" + decoded = base64.b64decode(value) + if encoding is None: + return decoded + return decoded.decode(encoding) diff --git a/homeassistant/helpers/template/extensions/crypto.py b/homeassistant/helpers/template/extensions/crypto.py new file mode 100644 index 00000000000000..c3ff165d7272d3 --- /dev/null +++ b/homeassistant/helpers/template/extensions/crypto.py @@ -0,0 +1,64 @@ +"""Cryptographic hash functions for Home Assistant templates.""" + +from __future__ import annotations + +import hashlib +from typing import TYPE_CHECKING + +from .base import BaseTemplateExtension, TemplateFunction + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +class CryptoExtension(BaseTemplateExtension): + """Jinja2 extension for cryptographic hash functions.""" + + def __init__(self, environment: TemplateEnvironment) -> None: + """Initialize the crypto extension.""" + super().__init__( + environment, + functions=[ + # Hash functions (as globals and filters) + TemplateFunction( + "md5", self.md5, as_global=True, as_filter=True, limited_ok=False + ), + TemplateFunction( + "sha1", self.sha1, as_global=True, as_filter=True, limited_ok=False + ), + TemplateFunction( + "sha256", + self.sha256, + as_global=True, + as_filter=True, + limited_ok=False, + ), + TemplateFunction( + "sha512", + self.sha512, + as_global=True, + as_filter=True, + limited_ok=False, + ), + ], + ) + + @staticmethod + def md5(value: str) -> str: + """Generate md5 hash from a string.""" + return hashlib.md5(value.encode()).hexdigest() + + @staticmethod + def sha1(value: str) -> str: + """Generate sha1 hash from a string.""" + return hashlib.sha1(value.encode()).hexdigest() + + @staticmethod + def sha256(value: str) -> str: + """Generate sha256 hash from a string.""" + return hashlib.sha256(value.encode()).hexdigest() + + @staticmethod + def sha512(value: str) -> str: + """Generate sha512 hash from a string.""" + return hashlib.sha512(value.encode()).hexdigest() diff --git a/tests/helpers/template/__init__.py b/tests/helpers/template/__init__.py new file mode 100644 index 00000000000000..f1e980fd2fb607 --- /dev/null +++ b/tests/helpers/template/__init__.py @@ -0,0 +1 @@ +"""Tests for Home Assistant template engine.""" diff --git a/tests/helpers/template/extensions/__init__.py b/tests/helpers/template/extensions/__init__.py new file mode 100644 index 00000000000000..43b7c1caccf0a7 --- /dev/null +++ b/tests/helpers/template/extensions/__init__.py @@ -0,0 +1 @@ +"""Tests for Home Assistant template extensions.""" diff --git a/tests/helpers/template/extensions/test_base64.py b/tests/helpers/template/extensions/test_base64.py new file mode 100644 index 00000000000000..b0c1fb35134d05 --- /dev/null +++ b/tests/helpers/template/extensions/test_base64.py @@ -0,0 +1,43 @@ +"""Test base64 encoding and decoding functions for Home Assistant templates.""" + +from __future__ import annotations + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import template + + +@pytest.mark.parametrize( + ("value_template", "expected"), + [ + ('{{ "homeassistant" | base64_encode }}', "aG9tZWFzc2lzdGFudA=="), + ("{{ int('0F010003', base=16) | pack('>I') | base64_encode }}", "DwEAAw=="), + ("{{ 'AA01000200150020' | from_hex | base64_encode }}", "qgEAAgAVACA="), + ], +) +def test_base64_encode(hass: HomeAssistant, value_template: str, expected: str) -> None: + """Test the base64_encode filter.""" + assert template.Template(value_template, hass).async_render() == expected + + +def test_base64_decode(hass: HomeAssistant) -> None: + """Test the base64_decode filter.""" + assert ( + template.Template( + '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', hass + ).async_render() + == "homeassistant" + ) + assert ( + template.Template( + '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}', hass + ).async_render() + == b"homeassistant" + ) + assert ( + template.Template( + '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}', hass + ).async_render() + == "homeassistant" + ) diff --git a/tests/helpers/template/extensions/test_crypto.py b/tests/helpers/template/extensions/test_crypto.py new file mode 100644 index 00000000000000..f1e4c3b39cc2b9 --- /dev/null +++ b/tests/helpers/template/extensions/test_crypto.py @@ -0,0 +1,58 @@ +"""Test cryptographic hash functions for Home Assistant templates.""" + +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import template + + +def test_md5(hass: HomeAssistant) -> None: + """Test the md5 function and filter.""" + assert ( + template.Template("{{ md5('Home Assistant') }}", hass).async_render() + == "3d15e5c102c3413d0337393c3287e006" + ) + + assert ( + template.Template("{{ 'Home Assistant' | md5 }}", hass).async_render() + == "3d15e5c102c3413d0337393c3287e006" + ) + + +def test_sha1(hass: HomeAssistant) -> None: + """Test the sha1 function and filter.""" + assert ( + template.Template("{{ sha1('Home Assistant') }}", hass).async_render() + == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" + ) + + assert ( + template.Template("{{ 'Home Assistant' | sha1 }}", hass).async_render() + == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" + ) + + +def test_sha256(hass: HomeAssistant) -> None: + """Test the sha256 function and filter.""" + assert ( + template.Template("{{ sha256('Home Assistant') }}", hass).async_render() + == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" + ) + + assert ( + template.Template("{{ 'Home Assistant' | sha256 }}", hass).async_render() + == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" + ) + + +def test_sha512(hass: HomeAssistant) -> None: + """Test the sha512 function and filter.""" + assert ( + template.Template("{{ sha512('Home Assistant') }}", hass).async_render() + == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" + ) + + assert ( + template.Template("{{ 'Home Assistant' | sha512 }}", hass).async_render() + == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" + ) diff --git a/tests/helpers/snapshots/test_template.ambr b/tests/helpers/template/snapshots/test_init.ambr similarity index 100% rename from tests/helpers/snapshots/test_template.ambr rename to tests/helpers/template/snapshots/test_init.ambr diff --git a/tests/helpers/test_template.py b/tests/helpers/template/test_init.py similarity index 98% rename from tests/helpers/test_template.py rename to tests/helpers/template/test_init.py index 85a2673f17d90f..6d4c27123fc266 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/template/test_init.py @@ -1739,41 +1739,6 @@ def test_from_hex(hass: HomeAssistant) -> None: ) -@pytest.mark.parametrize( - ("value_template", "expected"), - [ - ('{{ "homeassistant" | base64_encode }}', "aG9tZWFzc2lzdGFudA=="), - ("{{ int('0F010003', base=16) | pack('>I') | base64_encode }}", "DwEAAw=="), - ("{{ 'AA01000200150020' | from_hex | base64_encode }}", "qgEAAgAVACA="), - ], -) -def test_base64_encode(hass: HomeAssistant, value_template: str, expected: str) -> None: - """Test the base64_encode filter.""" - assert template.Template(value_template, hass).async_render() == expected - - -def test_base64_decode(hass: HomeAssistant) -> None: - """Test the base64_decode filter.""" - assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', hass - ).async_render() - == "homeassistant" - ) - assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode(None) }}', hass - ).async_render() - == b"homeassistant" - ) - assert ( - template.Template( - '{{ "aG9tZWFzc2lzdGFudA==" | base64_decode("ascii") }}', hass - ).async_render() - == "homeassistant" - ) - - def test_slugify(hass: HomeAssistant) -> None: """Test the slugify filter.""" assert ( @@ -7174,58 +7139,6 @@ def test_symmetric_difference(hass: HomeAssistant) -> None: ).async_render() -def test_md5(hass: HomeAssistant) -> None: - """Test the md5 function and filter.""" - assert ( - template.Template("{{ md5('Home Assistant') }}", hass).async_render() - == "3d15e5c102c3413d0337393c3287e006" - ) - - assert ( - template.Template("{{ 'Home Assistant' | md5 }}", hass).async_render() - == "3d15e5c102c3413d0337393c3287e006" - ) - - -def test_sha1(hass: HomeAssistant) -> None: - """Test the sha1 function and filter.""" - assert ( - template.Template("{{ sha1('Home Assistant') }}", hass).async_render() - == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha1 }}", hass).async_render() - == "c8fd3bb19b94312664faa619af7729bdbf6e9f8a" - ) - - -def test_sha256(hass: HomeAssistant) -> None: - """Test the sha256 function and filter.""" - assert ( - template.Template("{{ sha256('Home Assistant') }}", hass).async_render() - == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha256 }}", hass).async_render() - == "2a366abb0cd47f51f3725bf0fb7ebcb4fefa6e20f4971e25fe2bb8da8145ce2b" - ) - - -def test_sha512(hass: HomeAssistant) -> None: - """Test the sha512 function and filter.""" - assert ( - template.Template("{{ sha512('Home Assistant') }}", hass).async_render() - == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" - ) - - assert ( - template.Template("{{ 'Home Assistant' | sha512 }}", hass).async_render() - == "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb" - ) - - def test_combine(hass: HomeAssistant) -> None: """Test combine filter and function.""" assert template.Template(