diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 63cafce6c731f..81a327424feae 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 0d465f428a621..41a2c1c7ea1ec 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 044aea8d2cff8..c3a5073d03898 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 1997f1c02b0c6..801c4bb36bc95 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 d18726c8c793b..ec569f63ca317 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 fb5deb2958f1b..daaa737471370 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 beb14a80bed6a..1b78cae3e0fc1 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 f0e2572fa5414..86be8cd4da500 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 e0ffe2933e009..fb4cb43e7c043 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 7ac7c23981641..0292677ab9352 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 @@ -159,7 +159,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.07.0 + uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 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@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 @@ -219,7 +219,7 @@ jobs: sed -i "/uv/d" requirements_diff.txt - name: Build wheels - uses: home-assistant/wheels@2025.07.0 + uses: home-assistant/wheels@bf4ddde339dde61ba98ccb4330517936bed6d2f8 # 2025.07.0 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 diff --git a/CODEOWNERS b/CODEOWNERS index c4ce561fdb62d..bc3fd1b495f6f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -970,6 +970,8 @@ build.json @home-assistant/supervisor /tests/components/moat/ @bdraco /homeassistant/components/mobile_app/ @home-assistant/core /tests/components/mobile_app/ @home-assistant/core +/homeassistant/components/modbus/ @janiversen +/tests/components/modbus/ @janiversen /homeassistant/components/modem_callerid/ @tkdrob /tests/components/modem_callerid/ @tkdrob /homeassistant/components/modern_forms/ @wonderslug diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index c046933d5d5d2..bb453c67f57f8 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -2,21 +2,23 @@ from __future__ import annotations +import asyncio import logging from accuweather import AccuWeather from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM -from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION +from .const import DOMAIN from .coordinator import ( AccuWeatherConfigEntry, AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherData, + AccuWeatherHourlyForecastDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator, ) @@ -28,7 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] - name: str = entry.data[CONF_NAME] location_key = entry.unique_id @@ -41,26 +42,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) hass, entry, accuweather, - name, - "observation", - UPDATE_INTERVAL_OBSERVATION, ) - coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator( hass, entry, accuweather, - name, - "daily forecast", - UPDATE_INTERVAL_DAILY_FORECAST, + ) + coordinator_hourly_forecast = AccuWeatherHourlyForecastDataUpdateCoordinator( + hass, + entry, + accuweather, ) - await coordinator_observation.async_config_entry_first_refresh() - await coordinator_daily_forecast.async_config_entry_first_refresh() + await asyncio.gather( + coordinator_observation.async_config_entry_first_refresh(), + coordinator_daily_forecast.async_config_entry_first_refresh(), + coordinator_hourly_forecast.async_config_entry_first_refresh(), + ) entry.runtime_data = AccuWeatherData( coordinator_observation=coordinator_observation, coordinator_daily_forecast=coordinator_daily_forecast, + coordinator_hourly_forecast=coordinator_hourly_forecast, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index b9bf8df455618..a487e95582cd2 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -71,3 +71,4 @@ } UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) +UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30) diff --git a/homeassistant/components/accuweather/coordinator.py b/homeassistant/components/accuweather/coordinator.py index 780c977f9305a..7056c6e81fdb8 100644 --- a/homeassistant/components/accuweather/coordinator.py +++ b/homeassistant/components/accuweather/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import timeout +from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import timedelta import logging @@ -12,6 +13,7 @@ from aiohttp.client_exceptions import ClientConnectorError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -20,7 +22,13 @@ UpdateFailed, ) -from .const import DOMAIN, MANUFACTURER +from .const import ( + DOMAIN, + MANUFACTURER, + UPDATE_INTERVAL_DAILY_FORECAST, + UPDATE_INTERVAL_HOURLY_FORECAST, + UPDATE_INTERVAL_OBSERVATION, +) EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError) @@ -33,6 +41,7 @@ class AccuWeatherData: coordinator_observation: AccuWeatherObservationDataUpdateCoordinator coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator + coordinator_hourly_forecast: AccuWeatherHourlyForecastDataUpdateCoordinator type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData] @@ -48,13 +57,11 @@ def __init__( hass: HomeAssistant, config_entry: AccuWeatherConfigEntry, accuweather: AccuWeather, - name: str, - coordinator_type: str, - update_interval: timedelta, ) -> None: """Initialize.""" self.accuweather = accuweather self.location_key = accuweather.location_key + name = config_entry.data[CONF_NAME] if TYPE_CHECKING: assert self.location_key is not None @@ -65,8 +72,8 @@ def __init__( hass, _LOGGER, config_entry=config_entry, - name=f"{name} ({coordinator_type})", - update_interval=update_interval, + name=f"{name} (observation)", + update_interval=UPDATE_INTERVAL_OBSERVATION, ) async def _async_update_data(self) -> dict[str, Any]: @@ -86,23 +93,25 @@ async def _async_update_data(self) -> dict[str, Any]: return result -class AccuWeatherDailyForecastDataUpdateCoordinator( +class AccuWeatherForecastDataUpdateCoordinator( TimestampDataUpdateCoordinator[list[dict[str, Any]]] ): - """Class to manage fetching AccuWeather data API.""" + """Base class for AccuWeather forecast.""" def __init__( self, hass: HomeAssistant, config_entry: AccuWeatherConfigEntry, accuweather: AccuWeather, - name: str, coordinator_type: str, update_interval: timedelta, + fetch_method: Callable[..., Awaitable[list[dict[str, Any]]]], ) -> None: """Initialize.""" self.accuweather = accuweather self.location_key = accuweather.location_key + self._fetch_method = fetch_method + name = config_entry.data[CONF_NAME] if TYPE_CHECKING: assert self.location_key is not None @@ -118,12 +127,10 @@ def __init__( ) async def _async_update_data(self) -> list[dict[str, Any]]: - """Update data via library.""" + """Update forecast data via library.""" try: async with timeout(10): - result = await self.accuweather.async_get_daily_forecast( - language=self.hass.config.language - ) + result = await self._fetch_method(language=self.hass.config.language) except EXCEPTIONS as error: raise UpdateFailed( translation_domain=DOMAIN, @@ -132,10 +139,53 @@ async def _async_update_data(self) -> list[dict[str, Any]]: ) from error _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) - return result +class AccuWeatherDailyForecastDataUpdateCoordinator( + AccuWeatherForecastDataUpdateCoordinator +): + """Coordinator for daily forecast.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: AccuWeatherConfigEntry, + accuweather: AccuWeather, + ) -> None: + """Initialize.""" + super().__init__( + hass, + config_entry, + accuweather, + "daily forecast", + UPDATE_INTERVAL_DAILY_FORECAST, + fetch_method=accuweather.async_get_daily_forecast, + ) + + +class AccuWeatherHourlyForecastDataUpdateCoordinator( + AccuWeatherForecastDataUpdateCoordinator +): + """Coordinator for hourly forecast.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: AccuWeatherConfigEntry, + accuweather: AccuWeather, + ) -> None: + """Initialize.""" + super().__init__( + hass, + config_entry, + accuweather, + "hourly forecast", + UPDATE_INTERVAL_HOURLY_FORECAST, + fetch_method=accuweather.async_get_hourly_forecast, + ) + + def _get_device_info(location_key: str, name: str) -> DeviceInfo: """Get device info.""" return DeviceInfo( diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 770f2b64f2040..25d6297cee686 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -45,6 +45,7 @@ AccuWeatherConfigEntry, AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherData, + AccuWeatherHourlyForecastDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator, ) @@ -64,6 +65,7 @@ class AccuWeatherEntity( CoordinatorWeatherEntity[ AccuWeatherObservationDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherHourlyForecastDataUpdateCoordinator, ] ): """Define an AccuWeather entity.""" @@ -76,6 +78,7 @@ def __init__(self, accuweather_data: AccuWeatherData) -> None: super().__init__( observation_coordinator=accuweather_data.coordinator_observation, daily_coordinator=accuweather_data.coordinator_daily_forecast, + hourly_coordinator=accuweather_data.coordinator_hourly_forecast, ) self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS @@ -86,10 +89,13 @@ def __init__(self, accuweather_data: AccuWeatherData) -> None: self._attr_unique_id = accuweather_data.coordinator_observation.location_key self._attr_attribution = ATTRIBUTION self._attr_device_info = accuweather_data.coordinator_observation.device_info - self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + self._attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) self.observation_coordinator = accuweather_data.coordinator_observation self.daily_coordinator = accuweather_data.coordinator_daily_forecast + self.hourly_coordinator = accuweather_data.coordinator_hourly_forecast @property def condition(self) -> str | None: @@ -207,3 +213,32 @@ def _async_forecast_daily(self) -> list[Forecast] | None: } for item in self.daily_coordinator.data ] + + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return [ + { + ATTR_FORECAST_TIME: utc_from_timestamp( + item["EpochDateTime"] + ).isoformat(), + ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCover"], + ATTR_FORECAST_HUMIDITY: item["RelativeHumidity"], + ATTR_FORECAST_NATIVE_TEMP: item["Temperature"][ATTR_VALUE], + ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperature"][ + ATTR_VALUE + ], + ATTR_FORECAST_NATIVE_PRECIPITATION: item["TotalLiquid"][ATTR_VALUE], + ATTR_FORECAST_PRECIPITATION_PROBABILITY: item[ + "PrecipitationProbability" + ], + ATTR_FORECAST_NATIVE_WIND_SPEED: item["Wind"][ATTR_SPEED][ATTR_VALUE], + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGust"][ATTR_SPEED][ + ATTR_VALUE + ], + ATTR_FORECAST_UV_INDEX: item["UVIndex"], + ATTR_FORECAST_WIND_BEARING: item["Wind"][ATTR_DIRECTION]["Degrees"], + ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["WeatherIcon"]), + } + for item in self.hourly_coordinator.data + ] diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index adcc53bfc75c8..48bedafdd1ab8 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from genie_partner_sdk.client import AladdinConnectClient -from genie_partner_sdk.model import GarageDoor from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -36,22 +35,7 @@ async def async_setup_entry( api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session) ) - sdk_doors = await client.get_doors() - - # Convert SDK GarageDoor objects to integration GarageDoor objects - doors = [ - GarageDoor( - { - "device_id": door.device_id, - "door_number": door.door_number, - "name": door.name, - "status": door.status, - "link_status": door.link_status, - "battery_level": door.battery_level, - } - ) - for door in sdk_doors - ] + doors = await client.get_doors() entry.runtime_data = { door.unique_id: AladdinConnectCoordinator(hass, entry, client, door) diff --git a/homeassistant/components/aladdin_connect/coordinator.py b/homeassistant/components/aladdin_connect/coordinator.py index 74afbe8fca972..718aed8e44572 100644 --- a/homeassistant/components/aladdin_connect/coordinator.py +++ b/homeassistant/components/aladdin_connect/coordinator.py @@ -41,4 +41,10 @@ def __init__( async def _async_update_data(self) -> GarageDoor: """Fetch data from the Aladdin Connect API.""" await self.client.update_door(self.data.device_id, self.data.door_number) + self.data.status = self.client.get_door_status( + self.data.device_id, self.data.door_number + ) + self.data.battery_level = self.client.get_battery_status( + self.data.device_id, self.data.door_number + ) return self.data diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 7af0e4eb2cee2..4bc787539fd9d 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -49,7 +49,9 @@ async def async_close_cover(self, **kwargs: Any) -> None: @property def is_closed(self) -> bool | None: """Update is closed attribute.""" - return self.coordinator.data.status == "closed" + if (status := self.coordinator.data.status) is None: + return None + return status == "closed" @property def is_closing(self) -> bool | None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 8165ebd4ac991..e19d5c61d049f 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["genie-partner-sdk==1.0.10"] + "requirements": ["genie-partner-sdk==1.0.11"] } diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 4ffa0e24777d8..a5637053e4a48 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -33,9 +33,11 @@ ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ADB_SERVER_IP, @@ -46,10 +48,12 @@ DEFAULT_ADB_SERVER_PORT, DEVICE_ANDROIDTV, DEVICE_FIRETV, + DOMAIN, PROP_ETHMAC, PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) +from .services import async_setup_services ADB_PYTHON_EXCEPTIONS: tuple = ( AdbTimeoutError, @@ -63,6 +67,8 @@ ) ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] @@ -188,6 +194,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Android TV / Fire TV integration.""" + async_setup_services(hass) + return True + + async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool: """Set up Android Debug Bridge platform.""" diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 6a60d84e39ee4..9621282208e1e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -8,7 +8,6 @@ from androidtv.constants import APPS, KEYS from androidtv.setup_async import AndroidTVAsync, FireTVAsync -import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.media_player import ( @@ -17,9 +16,7 @@ MediaPlayerEntityFeature, MediaPlayerState, ) -from homeassistant.const import ATTR_COMMAND from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import utcnow @@ -39,19 +36,10 @@ SIGNAL_CONFIG_ENTITY, ) from .entity import AndroidTVEntity, adb_decorator +from .services import ATTR_ADB_RESPONSE, ATTR_HDMI_INPUT, SERVICE_LEARN_SENDEVENT _LOGGER = logging.getLogger(__name__) -ATTR_ADB_RESPONSE = "adb_response" -ATTR_DEVICE_PATH = "device_path" -ATTR_HDMI_INPUT = "hdmi_input" -ATTR_LOCAL_PATH = "local_path" - -SERVICE_ADB_COMMAND = "adb_command" -SERVICE_DOWNLOAD = "download" -SERVICE_LEARN_SENDEVENT = "learn_sendevent" -SERVICE_UPLOAD = "upload" - # Translate from `AndroidTV` / `FireTV` reported state to HA state. ANDROIDTV_STATES = { "off": MediaPlayerState.OFF, @@ -77,32 +65,6 @@ async def async_setup_entry( ] ) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - SERVICE_ADB_COMMAND, - {vol.Required(ATTR_COMMAND): cv.string}, - "adb_command", - ) - platform.async_register_entity_service( - SERVICE_LEARN_SENDEVENT, None, "learn_sendevent" - ) - platform.async_register_entity_service( - SERVICE_DOWNLOAD, - { - vol.Required(ATTR_DEVICE_PATH): cv.string, - vol.Required(ATTR_LOCAL_PATH): cv.string, - }, - "service_download", - ) - platform.async_register_entity_service( - SERVICE_UPLOAD, - { - vol.Required(ATTR_DEVICE_PATH): cv.string, - vol.Required(ATTR_LOCAL_PATH): cv.string, - }, - "service_upload", - ) - class ADBDevice(AndroidTVEntity, MediaPlayerEntity): """Representation of an Android or Fire TV device.""" diff --git a/homeassistant/components/androidtv/services.py b/homeassistant/components/androidtv/services.py new file mode 100644 index 0000000000000..8a44399b72746 --- /dev/null +++ b/homeassistant/components/androidtv/services.py @@ -0,0 +1,66 @@ +"""Services for Android/Fire TV devices.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.const import ATTR_COMMAND +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, service + +from .const import DOMAIN + +ATTR_ADB_RESPONSE = "adb_response" +ATTR_DEVICE_PATH = "device_path" +ATTR_HDMI_INPUT = "hdmi_input" +ATTR_LOCAL_PATH = "local_path" + +SERVICE_ADB_COMMAND = "adb_command" +SERVICE_DOWNLOAD = "download" +SERVICE_LEARN_SENDEVENT = "learn_sendevent" +SERVICE_UPLOAD = "upload" + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register the Android TV / Fire TV services.""" + + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_ADB_COMMAND, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={vol.Required(ATTR_COMMAND): cv.string}, + func="adb_command", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_LEARN_SENDEVENT, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema=None, + func="learn_sendevent", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_DOWNLOAD, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={ + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + }, + func="service_download", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_UPLOAD, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={ + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + }, + func="service_upload", + ) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 65a1e7010cf6d..e0aff037d9ea4 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["apcaccess"], "quality_scale": "platinum", - "requirements": ["aioapcaccess==0.4.2"] + "requirements": ["aioapcaccess==1.0.0"] } diff --git a/homeassistant/components/assist_pipeline/acknowledge.mp3 b/homeassistant/components/assist_pipeline/acknowledge.mp3 new file mode 100644 index 0000000000000..1709ff20bc2b7 Binary files /dev/null and b/homeassistant/components/assist_pipeline/acknowledge.mp3 differ diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 52583cf21a40b..54829a48f88c3 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -1,5 +1,7 @@ """Constants for the Assist pipeline integration.""" +from pathlib import Path + DOMAIN = "assist_pipeline" DATA_CONFIG = f"{DOMAIN}.config" @@ -23,3 +25,5 @@ BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit OPTION_PREFERRED = "preferred" + +ACKNOWLEDGE_PATH = Path(__file__).parent / "acknowledge.mp3" diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index ad291b3427bdc..8af0c9157b5ba 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -23,7 +23,12 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import chat_session, intent +from homeassistant.helpers import ( + chat_session, + device_registry as dr, + entity_registry as er, + intent, +) from homeassistant.helpers.collection import ( CHANGE_UPDATED, CollectionError, @@ -45,6 +50,7 @@ from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer from .const import ( + ACKNOWLEDGE_PATH, BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, @@ -113,6 +119,7 @@ def validate_language(data: dict[str, Any]) -> Any: vol.Required("wake_word_entity"): vol.Any(str, None), vol.Required("wake_word_id"): vol.Any(str, None), vol.Optional("prefer_local_intents"): bool, + vol.Optional("acknowledge_media_id"): str, } STORED_PIPELINE_RUNS = 10 @@ -1066,8 +1073,11 @@ async def recognize_intent( intent_input: str, conversation_id: str, conversation_extra_system_prompt: str | None, - ) -> str: - """Run intent recognition portion of pipeline. Returns text to speak.""" + ) -> tuple[str, bool]: + """Run intent recognition portion of pipeline. + + Returns (speech, all_targets_in_satellite_area). + """ if self.intent_agent is None or self._conversation_data is None: raise RuntimeError("Recognize intent was not prepared") @@ -1116,6 +1126,7 @@ async def recognize_intent( agent_id = self.intent_agent.id processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT + all_targets_in_satellite_area = False intent_response: intent.IntentResponse | None = None if not processed_locally and not self._intent_agent_only: # Sentence triggers override conversation agent @@ -1290,6 +1301,17 @@ async def tts_input_stream_generator() -> AsyncGenerator[str]: if tts_input_stream and self._streamed_response_text: tts_input_stream.put_nowait(None) + if agent_id == conversation.HOME_ASSISTANT_AGENT: + # Check if all targeted entities were in the same area as + # the satellite device. + # If so, the satellite should respond with an acknowledge beep + # instead of a full response. + all_targets_in_satellite_area = ( + self._get_all_targets_in_satellite_area( + conversation_result.response, self._device_id + ) + ) + except Exception as src_error: _LOGGER.exception("Unexpected error during intent recognition") raise IntentRecognitionError( @@ -1312,7 +1334,45 @@ async def tts_input_stream_generator() -> AsyncGenerator[str]: if conversation_result.continue_conversation: self._conversation_data.continue_conversation_agent = agent_id - return speech + return (speech, all_targets_in_satellite_area) + + def _get_all_targets_in_satellite_area( + self, intent_response: intent.IntentResponse, device_id: str | None + ) -> bool: + """Return true if all targeted entities were in the same area as the device.""" + if ( + (intent_response.response_type != intent.IntentResponseType.ACTION_DONE) + or (not intent_response.matched_states) + or (not device_id) + ): + return False + + device_registry = dr.async_get(self.hass) + + if (not (device := device_registry.async_get(device_id))) or ( + not device.area_id + ): + return False + + entity_registry = er.async_get(self.hass) + for state in intent_response.matched_states: + entity = entity_registry.async_get(state.entity_id) + if not entity: + return False + + if (entity_area_id := entity.area_id) is None: + if (entity.device_id is None) or ( + (entity_device := device_registry.async_get(entity.device_id)) + is None + ): + return False + + entity_area_id = entity_device.area_id + + if entity_area_id != device.area_id: + return False + + return True async def prepare_text_to_speech(self) -> None: """Prepare text-to-speech.""" @@ -1350,7 +1410,9 @@ async def prepare_text_to_speech(self) -> None: ), ) from err - async def text_to_speech(self, tts_input: str) -> None: + async def text_to_speech( + self, tts_input: str, override_media_path: Path | None = None + ) -> None: """Run text-to-speech portion of pipeline.""" assert self.tts_stream is not None @@ -1362,11 +1424,14 @@ async def text_to_speech(self, tts_input: str) -> None: "language": self.pipeline.tts_language, "voice": self.pipeline.tts_voice, "tts_input": tts_input, + "acknowledge_override": override_media_path is not None, }, ) ) - if not self._streamed_response_text: + if override_media_path: + self.tts_stream.async_override_result(override_media_path) + elif not self._streamed_response_text: self.tts_stream.async_set_message(tts_input) tts_output = { @@ -1664,16 +1729,20 @@ async def buffer_then_audio_stream() -> AsyncGenerator[ if self.run.end_stage != PipelineStage.STT: tts_input = self.tts_input + all_targets_in_satellite_area = False if current_stage == PipelineStage.INTENT: # intent-recognition assert intent_input is not None - tts_input = await self.run.recognize_intent( + ( + tts_input, + all_targets_in_satellite_area, + ) = await self.run.recognize_intent( intent_input, self.session.conversation_id, self.conversation_extra_system_prompt, ) - if tts_input.strip(): + if all_targets_in_satellite_area or tts_input.strip(): current_stage = PipelineStage.TTS else: # Skip TTS @@ -1682,8 +1751,14 @@ async def buffer_then_audio_stream() -> AsyncGenerator[ if self.run.end_stage != PipelineStage.INTENT: # text-to-speech if current_stage == PipelineStage.TTS: - assert tts_input is not None - await self.run.text_to_speech(tts_input) + if all_targets_in_satellite_area: + # Use acknowledge media instead of full response + await self.run.text_to_speech( + tts_input or "", override_media_path=ACKNOWLEDGE_PATH + ) + else: + assert tts_input is not None + await self.run.text_to_speech(tts_input) except PipelineError as err: self.run.process_event( diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 328672edbeda3..5a3f476a65da8 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -14,7 +14,11 @@ from elkm1_lib.zones import Zone import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.const import EntityCategory, UnitOfElectricPotential from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -32,6 +36,16 @@ SERVICE_SENSOR_ZONE_TRIGGER = "sensor_zone_trigger" UNDEFINED_TEMPERATURE = -40 +_DEVICE_CLASS_MAP: dict[ZoneType, SensorDeviceClass] = { + ZoneType.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + ZoneType.ANALOG_ZONE: SensorDeviceClass.VOLTAGE, +} + +_STATE_CLASS_MAP: dict[ZoneType, SensorStateClass] = { + ZoneType.TEMPERATURE: SensorStateClass.MEASUREMENT, + ZoneType.ANALOG_ZONE: SensorStateClass.MEASUREMENT, +} + ELK_SET_COUNTER_SERVICE_SCHEMA: VolDictType = { vol.Required(ATTR_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 65535)) } @@ -248,6 +262,16 @@ def temperature_unit(self) -> str | None: return self._temperature_unit return None + @property + def device_class(self) -> SensorDeviceClass | None: + """Return the device class of the sensor.""" + return _DEVICE_CLASS_MAP.get(self._element.definition) + + @property + def state_class(self) -> SensorStateClass | None: + """Return the state class of the sensor.""" + return _STATE_CLASS_MAP.get(self._element.definition) + @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" diff --git a/homeassistant/components/flexit_bacnet/sensor.py b/homeassistant/components/flexit_bacnet/sensor.py index 0506b13892b3f..8d4ec9ce80e1b 100644 --- a/homeassistant/components/flexit_bacnet/sensor.py +++ b/homeassistant/components/flexit_bacnet/sensor.py @@ -37,6 +37,7 @@ class FlexitSensorEntityDescription(SensorEntityDescription): FlexitSensorEntityDescription( key="outside_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key="outside_air_temperature", value_fn=lambda data: data.outside_air_temperature, @@ -44,6 +45,7 @@ class FlexitSensorEntityDescription(SensorEntityDescription): FlexitSensorEntityDescription( key="supply_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key="supply_air_temperature", value_fn=lambda data: data.supply_air_temperature, @@ -51,6 +53,7 @@ class FlexitSensorEntityDescription(SensorEntityDescription): FlexitSensorEntityDescription( key="exhaust_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key="exhaust_air_temperature", value_fn=lambda data: data.exhaust_air_temperature, @@ -58,6 +61,7 @@ class FlexitSensorEntityDescription(SensorEntityDescription): FlexitSensorEntityDescription( key="extract_air_temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key="extract_air_temperature", value_fn=lambda data: data.extract_air_temperature, @@ -65,6 +69,7 @@ class FlexitSensorEntityDescription(SensorEntityDescription): FlexitSensorEntityDescription( key="room_temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key="room_temperature", value_fn=lambda data: data.room_temperature, diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index d74bf1f30b7f9..44dff45029936 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250903.3"] + "requirements": ["home-assistant-frontend==20250903.5"] } diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index e378a1442d2e1..ce1a3e670a258 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.26"] + "requirements": ["pyiskra==0.1.27"] } diff --git a/homeassistant/components/litterrobot/icons.json b/homeassistant/components/litterrobot/icons.json index 91d48924ff331..1ee6b899905f4 100644 --- a/homeassistant/components/litterrobot/icons.json +++ b/homeassistant/components/litterrobot/icons.json @@ -31,6 +31,21 @@ "cycle_delay": { "default": "mdi:timer-outline" }, + "globe_brightness": { + "default": "mdi:lightbulb-question", + "state": { + "low": "mdi:lightbulb-on-30", + "medium": "mdi:lightbulb-on-50", + "high": "mdi:lightbulb-on" + } + }, + "globe_light": { + "state": { + "off": "mdi:lightbulb-off", + "on": "mdi:lightbulb-on", + "auto": "mdi:lightbulb-auto" + } + }, "meal_insert_size": { "default": "mdi:scale" } diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index be3a991594006..9ee186006b331 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -7,7 +7,7 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot -from pylitterbot.robot.litterrobot4 import BrightnessLevel +from pylitterbot.robot.litterrobot4 import BrightnessLevel, NightLightMode from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory, UnitOfTime @@ -32,35 +32,73 @@ class RobotSelectEntityDescription( select_fn: Callable[[_WhiskerEntityT, str], Coroutine[Any, Any, bool]] -ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { - LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check - key="cycle_delay", - translation_key="cycle_delay", - unit_of_measurement=UnitOfTime.MINUTES, - current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, - options_fn=lambda robot: robot.VALID_WAIT_TIMES, - select_fn=lambda robot, opt: robot.set_wait_time(int(opt)), +ROBOT_SELECT_MAP: dict[type[Robot], tuple[RobotSelectEntityDescription, ...]] = { + LitterRobot: ( + RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check + key="cycle_delay", + translation_key="cycle_delay", + unit_of_measurement=UnitOfTime.MINUTES, + current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, + options_fn=lambda robot: robot.VALID_WAIT_TIMES, + select_fn=lambda robot, opt: robot.set_wait_time(int(opt)), + ), ), - LitterRobot4: RobotSelectEntityDescription[LitterRobot4, str]( - key="panel_brightness", - translation_key="brightness_level", - current_fn=( - lambda robot: bri.name.lower() - if (bri := robot.panel_brightness) is not None - else None + LitterRobot4: ( + RobotSelectEntityDescription[LitterRobot4, str]( + key="globe_brightness", + translation_key="globe_brightness", + current_fn=( + lambda robot: bri.name.lower() + if (bri := robot.night_light_level) is not None + else None + ), + options_fn=lambda _: [level.name.lower() for level in BrightnessLevel], + select_fn=( + lambda robot, opt: robot.set_night_light_brightness( + BrightnessLevel[opt.upper()] + ) + ), + ), + RobotSelectEntityDescription[LitterRobot4, str]( + key="globe_light", + translation_key="globe_light", + current_fn=( + lambda robot: mode.name.lower() + if (mode := robot.night_light_mode) is not None + else None + ), + options_fn=lambda _: [mode.name.lower() for mode in NightLightMode], + select_fn=( + lambda robot, opt: robot.set_night_light_mode( + NightLightMode[opt.upper()] + ) + ), ), - options_fn=lambda _: [level.name.lower() for level in BrightnessLevel], - select_fn=( - lambda robot, opt: robot.set_panel_brightness(BrightnessLevel[opt.upper()]) + RobotSelectEntityDescription[LitterRobot4, str]( + key="panel_brightness", + translation_key="brightness_level", + current_fn=( + lambda robot: bri.name.lower() + if (bri := robot.panel_brightness) is not None + else None + ), + options_fn=lambda _: [level.name.lower() for level in BrightnessLevel], + select_fn=( + lambda robot, opt: robot.set_panel_brightness( + BrightnessLevel[opt.upper()] + ) + ), ), ), - FeederRobot: RobotSelectEntityDescription[FeederRobot, float]( - key="meal_insert_size", - translation_key="meal_insert_size", - unit_of_measurement="cups", - current_fn=lambda robot: robot.meal_insert_size, - options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, - select_fn=lambda robot, opt: robot.set_meal_insert_size(float(opt)), + FeederRobot: ( + RobotSelectEntityDescription[FeederRobot, float]( + key="meal_insert_size", + translation_key="meal_insert_size", + unit_of_measurement="cups", + current_fn=lambda robot: robot.meal_insert_size, + options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, + select_fn=lambda robot, opt: robot.set_meal_insert_size(float(opt)), + ), ), } @@ -77,8 +115,9 @@ async def async_setup_entry( robot=robot, coordinator=coordinator, description=description ) for robot in coordinator.account.robots - for robot_type, description in ROBOT_SELECT_MAP.items() + for robot_type, descriptions in ROBOT_SELECT_MAP.items() if isinstance(robot, robot_type) + for description in descriptions ) diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index e68e74011bdc7..5bb2d7ea9c72d 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -144,6 +144,22 @@ "cycle_delay": { "name": "Clean cycle wait time minutes" }, + "globe_brightness": { + "name": "Globe brightness", + "state": { + "low": "[%key:common::state::low%]", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]" + } + }, + "globe_light": { + "name": "Globe light", + "state": { + "auto": "[%key:common::state::auto%]", + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + }, "meal_insert_size": { "name": "Meal insert size" }, @@ -157,6 +173,9 @@ } }, "switch": { + "gravity_mode": { + "name": "Gravity mode" + }, "night_light_mode": { "name": "Night light mode" }, diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 5924f8f094aea..310859d98a2dd 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import Any, Generic -from pylitterbot import FeederRobot, LitterRobot +from pylitterbot import FeederRobot, LitterRobot, Robot from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory @@ -26,20 +26,30 @@ class RobotSwitchEntityDescription(SwitchEntityDescription, Generic[_WhiskerEnti value_fn: Callable[[_WhiskerEntityT], bool] -ROBOT_SWITCHES = [ - RobotSwitchEntityDescription[LitterRobot | FeederRobot]( - key="night_light_mode_enabled", - translation_key="night_light_mode", - set_fn=lambda robot, value: robot.set_night_light(value), - value_fn=lambda robot: robot.night_light_mode_enabled, +SWITCH_MAP: dict[type[Robot], tuple[RobotSwitchEntityDescription, ...]] = { + FeederRobot: ( + RobotSwitchEntityDescription[FeederRobot]( + key="gravity_mode", + translation_key="gravity_mode", + set_fn=lambda robot, value: robot.set_gravity_mode(value), + value_fn=lambda robot: robot.gravity_mode_enabled, + ), ), - RobotSwitchEntityDescription[LitterRobot | FeederRobot]( - key="panel_lock_enabled", - translation_key="panel_lockout", - set_fn=lambda robot, value: robot.set_panel_lockout(value), - value_fn=lambda robot: robot.panel_lock_enabled, + Robot: ( # type: ignore[type-abstract] # only used for isinstance check + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( + key="night_light_mode_enabled", + translation_key="night_light_mode", + set_fn=lambda robot, value: robot.set_night_light(value), + value_fn=lambda robot: robot.night_light_mode_enabled, + ), + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( + key="panel_lock_enabled", + translation_key="panel_lockout", + set_fn=lambda robot, value: robot.set_panel_lockout(value), + value_fn=lambda robot: robot.panel_lock_enabled, + ), ), -] +} async def async_setup_entry( @@ -51,9 +61,10 @@ async def async_setup_entry( coordinator = entry.runtime_data async_add_entities( RobotSwitchEntity(robot=robot, coordinator=coordinator, description=description) - for description in ROBOT_SWITCHES for robot in coordinator.account.robots - if isinstance(robot, (LitterRobot, FeederRobot)) + for robot_type, entity_descriptions in SWITCH_MAP.items() + if isinstance(robot, robot_type) + for description in entity_descriptions ) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 9039c3e9e2480..ea4491ebc79fa 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -16,6 +16,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, @@ -278,13 +279,18 @@ def native_unit_of_measurement(self) -> str | None: @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" + attributes: dict[str, list[str] | str | None] = { + ATTR_ENTITY_ID: self._entity_ids + } + if self._sensor_type == "min": - return {ATTR_MIN_ENTITY_ID: self.min_entity_id} - if self._sensor_type == "max": - return {ATTR_MAX_ENTITY_ID: self.max_entity_id} - if self._sensor_type == "last": - return {ATTR_LAST_ENTITY_ID: self.last_entity_id} - return None + attributes[ATTR_MIN_ENTITY_ID] = self.min_entity_id + elif self._sensor_type == "max": + attributes[ATTR_MAX_ENTITY_ID] = self.max_entity_id + elif self._sensor_type == "last": + attributes[ATTR_LAST_ENTITY_ID] = self.last_entity_id + + return attributes @callback def _async_min_max_sensor_state_listener( diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 32a043c43793a..429633224239e 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -1,9 +1,9 @@ { "domain": "modbus", "name": "Modbus", - "codeowners": [], + "codeowners": ["@janiversen"], "documentation": "https://www.home-assistant.io/integrations/modbus", "iot_class": "local_polling", "loggers": ["pymodbus"], - "requirements": ["pymodbus==3.11.1"] + "requirements": ["pymodbus==3.11.2"] } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index a1804efbca039..e873d53878d21 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -253,7 +253,6 @@ def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None: self._client: ( AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None ) = None - self._lock = asyncio.Lock() self.event_connected = asyncio.Event() self.hass = hass self.name = client_config[CONF_NAME] @@ -362,16 +361,13 @@ async def async_close(self) -> None: if not self._connect_task.done(): self._connect_task.cancel() - async with self._lock: - if self._client: - try: - self._client.close() - except ModbusException as exception_error: - self._log_error(str(exception_error)) - del self._client - self._client = None - message = f"modbus {self.name} communication closed" - _LOGGER.info(message) + if self._client: + try: + self._client.close() + except ModbusException as exception_error: + self._log_error(str(exception_error)) + self._client = None + _LOGGER.info(f"modbus {self.name} communication closed") async def low_level_pb_call( self, slave: int | None, address: int, value: int | list[int], use_call: str @@ -417,11 +413,9 @@ async def async_pb_call( use_call: str, ) -> ModbusPDU | None: """Convert async to sync pymodbus call.""" - async with self._lock: - if not self._client: - return None - result = await self.low_level_pb_call(unit, address, value, use_call) - if self._msg_wait: - # small delay until next request/response - await asyncio.sleep(self._msg_wait) - return result + if not self._client: + return None + result = await self.low_level_pb_call(unit, address, value, use_call) + if self._msg_wait: + await asyncio.sleep(self._msg_wait) + return result diff --git a/homeassistant/components/motionmount/select.py b/homeassistant/components/motionmount/select.py index 861faa319cd50..d02b286c2962f 100644 --- a/homeassistant/components/motionmount/select.py +++ b/homeassistant/components/motionmount/select.py @@ -36,6 +36,7 @@ class MotionMountPresets(MotionMountEntity, SelectEntity): _attr_should_poll = True _attr_translation_key = "motionmount_preset" + _name_to_index: dict[str, int] def __init__( self, @@ -50,8 +51,12 @@ def __init__( def _update_options(self, presets: list[motionmount.Preset]) -> None: """Convert presets to select options.""" - options = [f"{preset.index}: {preset.name}" for preset in presets] - options.insert(0, WALL_PRESET_NAME) + # Ordered list of options (wall first, then presets) + options = [WALL_PRESET_NAME] + [preset.name for preset in presets] + + # Build mapping name → index (wall = 0) + self._name_to_index = {WALL_PRESET_NAME: 0} + self._name_to_index.update({preset.name: preset.index for preset in presets}) self._attr_options = options @@ -123,7 +128,10 @@ def current_option(self) -> str | None: async def async_select_option(self, option: str) -> None: """Set the new option.""" - index = int(option[:1]) + index = self._name_to_index.get(option) + if index is None: + raise HomeAssistantError(f"Unknown preset selected: {option}") + try: await self.mm.go_to_preset(index) except (TimeoutError, socket.gaierror) as ex: diff --git a/homeassistant/components/motionmount/strings.json b/homeassistant/components/motionmount/strings.json index 2c951a7aefef5..8d079dd777d51 100644 --- a/homeassistant/components/motionmount/strings.json +++ b/homeassistant/components/motionmount/strings.json @@ -83,7 +83,7 @@ "motionmount_preset": { "name": "Preset", "state": { - "0_wall": "0: Wall" + "0_wall": "Wall" } } } diff --git a/homeassistant/components/number/icons.json b/homeassistant/components/number/icons.json index 482b4bc679321..9d75e09a72ddb 100644 --- a/homeassistant/components/number/icons.json +++ b/homeassistant/components/number/icons.json @@ -147,6 +147,9 @@ "volume": { "default": "mdi:car-coolant-level" }, + "volume_flow_rate": { + "default": "mdi:pipe-valve" + }, "volume_storage": { "default": "mdi:storage-tank" }, diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 7f5f4ad85bd40..99b7d48dcca77 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -100,6 +100,7 @@ UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.DISCRETE_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.EVO_HOME_CONTROLLER: Platform.CLIMATE, # widgetName, uiClass is EvoHome (not supported) diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index d14b2792947bc..9260f9800a11f 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -100,6 +100,15 @@ class OverkizSwitchDescription(SwitchEntityDescription): ), entity_category=EntityCategory.CONFIG, ), + OverkizSwitchDescription( + key=UIWidget.DISCRETE_EXTERIOR_HEATING, + turn_on=OverkizCommand.ON, + turn_off=OverkizCommand.OFF, + icon="mdi:radiator", + is_on=lambda select_state: ( + select_state(OverkizState.CORE_ON_OFF) == OverkizCommandParam.ON + ), + ), ] SUPPORTED_DEVICES = { diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index 6a35bf7923392..e5f449d498476 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -35,6 +35,7 @@ from .const import ( CONF_BASE_URL, CONF_ENTRY_CODE, + CONF_SHOW_BACKGROUND, CONF_USER_DATA, DEFAULT_DRAWABLES, DOMAIN, @@ -215,6 +216,7 @@ async def async_step_drawables( ) -> ConfigFlowResult: """Manage the map object drawable options.""" if user_input is not None: + self.options[CONF_SHOW_BACKGROUND] = user_input.pop(CONF_SHOW_BACKGROUND) self.options.setdefault(DRAWABLES, {}).update(user_input) return self.async_create_entry(title="", data=self.options) data_schema = {} @@ -227,6 +229,12 @@ async def async_step_drawables( ), ) ] = bool + data_schema[ + vol.Required( + CONF_SHOW_BACKGROUND, + default=self.config_entry.options.get(CONF_SHOW_BACKGROUND, False), + ) + ] = bool return self.async_show_form( step_id=DRAWABLES, data_schema=vol.Schema(data_schema), diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index e56fade7078ea..3ddce364e9f31 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -10,6 +10,7 @@ CONF_ENTRY_CODE = "code" CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" +CONF_SHOW_BACKGROUND = "show_background" # Option Flow steps DRAWABLES = "drawables" diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index dc0677b25d2ff..02d5f68466808 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -26,7 +26,7 @@ from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.version_a01_apis import RoborockClientA01 from roborock.web_api import RoborockApiClient -from vacuum_map_parser_base.config.color import ColorsPalette +from vacuum_map_parser_base.config.color import ColorsPalette, SupportedColor from vacuum_map_parser_base.config.image_config import ImageConfig from vacuum_map_parser_base.config.size import Size, Sizes from vacuum_map_parser_base.map_data import MapData @@ -44,6 +44,7 @@ from .const import ( A01_UPDATE_INTERVAL, + CONF_SHOW_BACKGROUND, DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, @@ -146,8 +147,11 @@ def __init__( for drawable, default_value in DEFAULT_DRAWABLES.items() if config_entry.options.get(DRAWABLES, {}).get(drawable, default_value) ] + colors = ColorsPalette() + if not config_entry.options.get(CONF_SHOW_BACKGROUND, False): + colors = ColorsPalette({SupportedColor.MAP_OUTSIDE: (0, 0, 0, 0)}) self.map_parser = RoborockMapDataParser( - ColorsPalette(), + colors, Sizes( { k: v * MAP_SCALE diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 2d1fcebd9d370..0eff2287a73e1 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -60,7 +60,8 @@ "room_names": "Room names", "vacuum_position": "Vacuum position", "virtual_walls": "Virtual walls", - "zones": "Zones" + "zones": "Zones", + "show_background": "Show background" }, "data_description": { "charger": "Show the charger on the map.", @@ -79,7 +80,8 @@ "room_names": "Show room names on the map.", "vacuum_position": "Show the vacuum position on the map.", "virtual_walls": "Show virtual walls on the map.", - "zones": "Show zones on the map." + "zones": "Show zones on the map.", + "show_background": "Add a background to the map." } } } diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index cea955e061c45..740b2df7e5b37 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -169,6 +169,9 @@ "volume": { "default": "mdi:car-coolant-level" }, + "volume_flow_rate": { + "default": "mdi:pipe-valve" + }, "volume_storage": { "default": "mdi:storage-tank" }, diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index bb8c9971433a2..af34119290b5e 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -11,6 +11,7 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.button import ( + DOMAIN as BUTTON_PLATFORM, ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, @@ -26,7 +27,14 @@ from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import get_entity_block_device_info, get_entity_rpc_device_info -from .utils import get_blu_trv_device_info, get_device_entry_gen, get_rpc_key_ids +from .utils import ( + async_remove_orphaned_entities, + get_blu_trv_device_info, + get_device_entry_gen, + get_rpc_entity_name, + get_rpc_key_ids, + get_virtual_component_ids, +) PARALLEL_UPDATES = 0 @@ -87,6 +95,13 @@ class ShellyButtonDescription[ ), ] +VIRTUAL_BUTTONS: Final[list[ShellyButtonDescription]] = [ + ShellyButtonDescription[ShellyRpcCoordinator]( + key="button", + press_action="single_push", + ) +] + @callback def async_migrate_unique_ids( @@ -138,7 +153,7 @@ async def async_setup_entry( hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator) ) - entities: list[ShellyButton | ShellyBluTrvButton] = [] + entities: list[ShellyButton | ShellyBluTrvButton | ShellyVirtualButton] = [] entities.extend( ShellyButton(coordinator, button) @@ -146,10 +161,20 @@ async def async_setup_entry( if button.supported(coordinator) ) - if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER): - if TYPE_CHECKING: - assert isinstance(coordinator, ShellyRpcCoordinator) + if not isinstance(coordinator, ShellyRpcCoordinator): + async_add_entities(entities) + return + + # add virtual buttons + if virtual_button_ids := get_rpc_key_ids(coordinator.device.status, "button"): + entities.extend( + ShellyVirtualButton(coordinator, button, id_) + for id_ in virtual_button_ids + for button in VIRTUAL_BUTTONS + ) + # add BLU TRV buttons + if blutrv_key_ids := get_rpc_key_ids(coordinator.device.status, BLU_TRV_IDENTIFIER): entities.extend( ShellyBluTrvButton(coordinator, button, id_) for id_ in blutrv_key_ids @@ -159,6 +184,19 @@ async def async_setup_entry( async_add_entities(entities) + # the user can remove virtual components from the device configuration, so + # we need to remove orphaned entities + virtual_button_component_ids = get_virtual_component_ids( + coordinator.device.config, BUTTON_PLATFORM + ) + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + BUTTON_PLATFORM, + virtual_button_component_ids, + ) + class ShellyBaseButton( CoordinatorEntity[ShellyRpcCoordinator | ShellyBlockCoordinator], ButtonEntity @@ -273,3 +311,32 @@ async def _press_method(self) -> None: assert method is not None await method(self._id) + + +class ShellyVirtualButton(ShellyBaseButton): + """Defines a Shelly virtual component button.""" + + def __init__( + self, + coordinator: ShellyRpcCoordinator, + description: ShellyButtonDescription, + _id: int, + ) -> None: + """Initialize Shelly virtual component button.""" + super().__init__(coordinator, description) + + self._attr_unique_id = f"{coordinator.mac}-{description.key}:{_id}" + self._attr_device_info = get_entity_rpc_device_info(coordinator) + self._attr_name = get_rpc_entity_name( + coordinator.device, f"{description.key}:{_id}" + ) + self._id = _id + + async def _press_method(self) -> None: + """Press method.""" + if TYPE_CHECKING: + assert isinstance(self.coordinator, ShellyRpcCoordinator) + + await self.coordinator.device.button_trigger( + self._id, self.entity_description.press_action + ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index bfa4718fb2e40..7a88f0d7c8db2 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -265,9 +265,10 @@ class BLEScannerMode(StrEnum): CONF_GEN = "gen" -VIRTUAL_COMPONENTS = ("boolean", "enum", "input", "number", "text") +VIRTUAL_COMPONENTS = ("boolean", "button", "enum", "input", "number", "text") VIRTUAL_COMPONENTS_MAP = { "binary_sensor": {"types": ["boolean"], "modes": ["label"]}, + "button": {"types": ["button"], "modes": ["button"]}, "number": {"types": ["number"], "modes": ["field", "slider"]}, "select": {"types": ["enum"], "modes": ["dropdown"]}, "sensor": {"types": ["enum", "number", "text"], "modes": ["label"]}, diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index eba6b846fe404..69c2d5c33de17 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -631,6 +631,11 @@ def _async_device_event_handler(self, event_data: dict[str, Any]) -> None: """Handle device events.""" events: list[dict[str, Any]] = event_data["events"] for event in events: + # filter out button events as they are triggered by button entities + component = event.get("component") + if component is not None and component.startswith("button"): + continue + event_type = event.get("event") if event_type is None: continue diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index ea1701b77a401..9c474e628737d 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tibber", "iot_class": "cloud_polling", "loggers": ["tibber"], - "requirements": ["pyTibber==0.31.7"] + "requirements": ["pyTibber==0.32.0"] } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 1c56d5b2ce6eb..b087ef406a19f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -377,7 +377,6 @@ def __init__(self, tibber_home: tibber.TibberHome) -> None: "app_nickname": None, "grid_company": None, "estimated_annual_consumption": None, - "price_level": None, "max_price": None, "avg_price": None, "min_price": None, @@ -405,16 +404,16 @@ async def async_update(self) -> None: await self._fetch_data() elif ( - self._tibber_home.current_price_total + self._tibber_home.price_total and self._last_updated and self._last_updated.hour == now.hour + and now - self._last_updated < timedelta(minutes=15) and self._tibber_home.last_data_timestamp ): return res = self._tibber_home.current_price_data() - self._attr_native_value, price_level, self._last_updated, price_rank = res - self._attr_extra_state_attributes["price_level"] = price_level + self._attr_native_value, self._last_updated, price_rank = res self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank attrs = self._tibber_home.current_attributes() diff --git a/homeassistant/components/tibber/services.py b/homeassistant/components/tibber/services.py index 938e96b991718..d5bb3fd4854b9 100644 --- a/homeassistant/components/tibber/services.py +++ b/homeassistant/components/tibber/services.py @@ -50,7 +50,6 @@ async def __get_prices(call: ServiceCall) -> ServiceResponse: { "start_time": starts_at, "price": price, - "level": tibber_home.price_level.get(starts_at), } for starts_at, price in tibber_home.price_total.items() ] diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 77dd8a2fefd9c..862e10c6fa14c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -331,6 +331,7 @@ class DPCode(StrEnum): SMOKE_SENSOR_STATE = "smoke_sensor_state" SMOKE_SENSOR_STATUS = "smoke_sensor_status" SMOKE_SENSOR_VALUE = "smoke_sensor_value" + SNOOZE = "snooze" SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level @@ -371,6 +372,7 @@ class DPCode(StrEnum): SWITCH_MODE7 = "switch_mode7" SWITCH_MODE8 = "switch_mode8" SWITCH_MODE9 = "switch_mode9" + SWITCH_MUSIC = "switch_music" SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SAVE_ENERGY = "switch_save_energy" SWITCH_SOUND = "switch_sound" # Voice switch @@ -384,6 +386,7 @@ class DPCode(StrEnum): SWITCH_VERTICAL = "switch_vertical" # Vertical swing flap switch SWITCH_VOICE = "switch_voice" # Voice switch TARGET_DIS_CLOSEST = "target_dis_closest" # Closest target distance + TDS_IN = "tds_in" # Total dissolved solids TEMP = "temp" # Temperature setting TEMP_BOILING_C = "temp_boiling_c" TEMP_BOILING_F = "temp_boiling_f" @@ -424,6 +427,7 @@ class DPCode(StrEnum): TOTAL_POWER = "total_power" TOTAL_TIME = "total_time" TVOC = "tvoc" + UP_DOWN = "up_down" UPPER_TEMP = "upper_temp" UPPER_TEMP_F = "upper_temp_f" UV = "uv" # UV sterilization diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 7d51a006877b4..1ed9aae1f2219 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -126,7 +126,7 @@ def find_dpcode( return None def get_dptype( - self, dpcode: DPCode | None, prefer_function: bool = False + self, dpcode: DPCode | None, *, prefer_function: bool = False ) -> DPType | None: """Find a matching DPCode data type available on for this device.""" if dpcode is None: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 673e9b1ffb300..9dba24ec490b8 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -73,6 +73,15 @@ class TuyaLightEntityDescription(LightEntityDescription): LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { + # White noise machine + "bzyd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + name=None, + color_mode=DPCode.WORK_MODE, + color_data=DPCode.COLOUR_DATA, + ), + ), # Curtain Switch # https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39 "clkg": ( @@ -531,7 +540,7 @@ def __init__( if ( dpcode := get_dpcode(self.device, description.color_data) - ) and self.get_dptype(dpcode) == DPType.JSON: + ) and self.get_dptype(dpcode, prefer_function=True) == DPType.JSON: self._color_data_dpcode = dpcode color_modes.add(ColorMode.HS) if dpcode in self.device.function: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 3ee6900d228ad..6a4482821bada 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -65,6 +65,14 @@ entity_category=EntityCategory.CONFIG, ), ), + # White noise machine + "bzyd": ( + NumberEntityDescription( + key=DPCode.VOLUME_SET, + translation_key="volume", + entity_category=EntityCategory.CONFIG, + ), + ), # CO2 Detector # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy "co2bj": ( diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 8b62ed36a52f7..0d62620b88e52 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -254,6 +254,11 @@ translation_key="desk_level", entity_category=EntityCategory.CONFIG, ), + SelectEntityDescription( + key=DPCode.UP_DOWN, + translation_key="desk_up_down", + entity_category=EntityCategory.CONFIG, + ), ), # Smart Camera # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index cac5d17e74dfa..021830b207342 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1161,6 +1161,21 @@ class TuyaSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, ), ), + # Water tester + "szjcy": ( + TuyaSensorEntityDescription( + key=DPCode.TDS_IN, + translation_key="total_dissolved_solids", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Fingerbot "szjqr": BATTERY_SENSORS, # IoT Switch diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index d5d9bdaeeed15..bdb10d7984b26 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -534,6 +534,14 @@ "level_4": "Level 4" } }, + "desk_up_down": { + "name": "Up/Down", + "state": { + "up": "Up", + "down": "Down", + "stop": "Stop" + } + }, "inverter_work_mode": { "name": "Inverter work mode", "state": { @@ -812,6 +820,9 @@ }, "supply_frequency": { "name": "Supply frequency" + }, + "total_dissolved_solids": { + "name": "Total dissolved solids" } }, "switch": { @@ -973,6 +984,12 @@ }, "output_power_limit": { "name": "Output power limit" + }, + "music": { + "name": "Music" + }, + "snooze": { + "name": "Snooze" } }, "valve": { diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 62ea4d86b3d0c..208cd3e19b7b3 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -37,6 +37,31 @@ entity_category=EntityCategory.CONFIG, ), ), + # White noise machine + "bzyd": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name=None, + ), + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + translation_key="child_lock", + icon="mdi:account-lock", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_MUSIC, + translation_key="music", + icon="mdi:music", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SNOOZE, + translation_key="snooze", + icon="mdi:alarm-snooze", + entity_category=EntityCategory.CONFIG, + ), + ), # Curtain # https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc "cl": ( diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 907123561f79f..3cc27a6f7e138 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -12,8 +12,9 @@ from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.template import Template from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ( DEFAULT_METHODS, @@ -33,7 +34,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "webhook", - vol.Required(CONF_WEBHOOK_ID): cv.string, + vol.Required(CONF_WEBHOOK_ID): cv.template, vol.Optional(CONF_ALLOWED_METHODS): vol.All( cv.ensure_list, [vol.All(vol.Upper, vol.In(SUPPORTED_METHODS))], @@ -83,7 +84,13 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - webhook_id: str = config[CONF_WEBHOOK_ID] + variables: TemplateVarsType | None = None + if trigger_info: + variables = trigger_info.get("variables") + webhook_id_template: Template = config[CONF_WEBHOOK_ID] + webhook_id: str = webhook_id_template.async_render( + variables, limited=True, parse_result=False + ) local_only = config.get(CONF_LOCAL_ONLY, True) allowed_methods = config.get(CONF_ALLOWED_METHODS, DEFAULT_METHODS) job = HassJob(action) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index a412d475acff8..5b21c12d75533 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -33,6 +33,7 @@ entity_registry, floor_registry, ) +from .deprecation import EnumWithDeprecatedMembers from .typing import VolSchemaType _LOGGER = logging.getLogger(__name__) @@ -1316,14 +1317,23 @@ def create_response(self) -> IntentResponse: return IntentResponse(language=self.language, intent=self) -class IntentResponseType(Enum): +class IntentResponseType( + Enum, + metaclass=EnumWithDeprecatedMembers, + deprecated={ + "PARTIAL_ACTION_DONE": ( + "IntentResponseType.ACTION_DONE or IntentResponseType.ERROR", + "2026.3.0", + ), + }, +): """Type of the intent response.""" ACTION_DONE = "action_done" """Intent caused an action to occur""" PARTIAL_ACTION_DONE = "partial_action_done" - """Intent caused an action, but it could only be partially done""" + """Deprecated. Intent caused an action, but it could only be partially done""" QUERY_ANSWER = "query_answer" """Response is an answer to a query""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dee918c3f66ce..98622eab1d208 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==5.6.2 hass-nabucasa==1.1.1 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 home-assistant-intents==2025.9.3 httpx==0.28.1 ifaddr==0.2.0 @@ -219,7 +219,7 @@ num2words==0.5.14 # pymodbus does not follow SemVer, and it keeps getting # downgraded or upgraded by custom components # This ensures all use the same version -pymodbus==3.11.1 +pymodbus==3.11.2 # Some packages don't support gql 4.0.0 yet gql<4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index d875524f13e0c..a192b85e0c1d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aioamazondevices==6.0.0 aioambient==2024.08.0 # homeassistant.components.apcupsd -aioapcaccess==0.4.2 +aioapcaccess==1.0.0 # homeassistant.components.aquacell aioaquacell==0.2.0 @@ -1005,7 +1005,7 @@ gassist-text==0.0.14 gcal-sync==8.0.0 # homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.10 +genie-partner-sdk==1.0.11 # homeassistant.components.geniushub geniushub-client==0.7.1 @@ -1181,7 +1181,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 # homeassistant.components.conversation home-assistant-intents==2025.9.3 @@ -1822,7 +1822,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.31.7 +pyTibber==0.32.0 # homeassistant.components.dlink pyW215==0.8.0 @@ -2067,7 +2067,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.26 +pyiskra==0.1.27 # homeassistant.components.iss pyiss==1.0.1 @@ -2163,7 +2163,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.11.1 +pymodbus==3.11.2 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 531a4fee32763..b026a547cfc9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aioamazondevices==6.0.0 aioambient==2024.08.0 # homeassistant.components.apcupsd -aioapcaccess==0.4.2 +aioapcaccess==1.0.0 # homeassistant.components.aquacell aioaquacell==0.2.0 @@ -875,7 +875,7 @@ gassist-text==0.0.14 gcal-sync==8.0.0 # homeassistant.components.aladdin_connect -genie-partner-sdk==1.0.10 +genie-partner-sdk==1.0.11 # homeassistant.components.geniushub geniushub-client==0.7.1 @@ -1030,7 +1030,7 @@ hole==0.9.0 holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250903.3 +home-assistant-frontend==20250903.5 # homeassistant.components.conversation home-assistant-intents==2025.9.3 @@ -1533,7 +1533,7 @@ pyHomee==1.3.8 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.31.7 +pyTibber==0.32.0 # homeassistant.components.dlink pyW215==0.8.0 @@ -1721,7 +1721,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.26 +pyiskra==0.1.27 # homeassistant.components.iss pyiss==1.0.1 @@ -1802,7 +1802,7 @@ pymiele==0.5.4 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.11.1 +pymodbus==3.11.2 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 8a6c09ff3a4c0..e482c01b3dd60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -245,7 +245,7 @@ # pymodbus does not follow SemVer, and it keeps getting # downgraded or upgraded by custom components # This ensures all use the same version -pymodbus==3.11.1 +pymodbus==3.11.2 # Some packages don't support gql 4.0.0 yet gql<4.0.0 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 2c34cf36c88cf..a3a0f9d6facb0 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -2293,7 +2293,11 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: ): integration.add_error( "quality_scale", - "Quality scale definition not found. New integrations are required to at least reach the Bronze tier.", + ( + "New integrations marked as internal should be added to INTEGRATIONS_WITHOUT_SCALE in script/hassfest/quality_scale.py." + if integration.quality_scale == "internal" + else "Quality scale definition not found. New integrations are required to at least reach the Bronze tier." + ), ) return if declared_quality_scale is not None: @@ -2338,7 +2342,11 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: ): integration.add_error( "quality_scale", - "New integrations are required to at least reach the Bronze tier.", + ( + "New integrations marked as internal should be added to INTEGRATIONS_WITHOUT_SCALE in script/hassfest/quality_scale.py." + if integration.quality_scale == "internal" + else "New integrations are required to at least reach the Bronze tier." + ), ) return name = str(iqs_file) diff --git a/tests/components/accuweather/conftest.py b/tests/components/accuweather/conftest.py index 737fd3f84b66e..abecc7cc198ee 100644 --- a/tests/components/accuweather/conftest.py +++ b/tests/components/accuweather/conftest.py @@ -14,7 +14,8 @@ def mock_accuweather_client() -> Generator[AsyncMock]: """Mock a AccuWeather client.""" current = load_json_object_fixture("current_conditions_data.json", DOMAIN) - forecast = load_json_array_fixture("forecast_data.json", DOMAIN) + daily_forecast = load_json_array_fixture("daily_forecast_data.json", DOMAIN) + hourly_forecast = load_json_array_fixture("hourly_forecast_data.json", DOMAIN) location = load_json_object_fixture("location_data.json", DOMAIN) with ( @@ -29,7 +30,8 @@ def mock_accuweather_client() -> Generator[AsyncMock]: client = mock_client.return_value client.async_get_location.return_value = location client.async_get_current_conditions.return_value = current - client.async_get_daily_forecast.return_value = forecast + client.async_get_daily_forecast.return_value = daily_forecast + client.async_get_hourly_forecast.return_value = hourly_forecast client.location_key = "0123456" client.requests_remaining = 10 diff --git a/tests/components/accuweather/fixtures/forecast_data.json b/tests/components/accuweather/fixtures/daily_forecast_data.json similarity index 100% rename from tests/components/accuweather/fixtures/forecast_data.json rename to tests/components/accuweather/fixtures/daily_forecast_data.json diff --git a/tests/components/accuweather/fixtures/hourly_forecast_data.json b/tests/components/accuweather/fixtures/hourly_forecast_data.json new file mode 100644 index 0000000000000..43a04d533a143 --- /dev/null +++ b/tests/components/accuweather/fixtures/hourly_forecast_data.json @@ -0,0 +1,1334 @@ +[ + { + "DateTime": "2025-09-12t16:00:00+02:00", + "EpochDateTime": 1757685600, + "WeatherIcon": 2, + "IconPhrase": "przewa\u017cnie s\u0142onecznie", + "HasPrecipitation": false, + "IsDaylight": true, + "Temperature": { + "Value": 22.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 22.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "RealFeelTemperatureShade": { + "Value": 20.8, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "WetBulbTemperature": { + "Value": 15.6, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 19.4, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 11.1, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 239, + "Localized": "WSW", + "English": "WSW" + } + }, + "WindGust": { + "Speed": { + "Value": 24.1, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 48, + "IndoorRelativeHumidity": 48, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 10058.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 2, + "UVIndexFloat": 2.4, + "UVIndexText": "niskie", + "PrecipitationProbability": 1, + "ThunderstormProbability": 0, + "RainProbability": 1, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 13, + "Evapotranspiration": { + "Value": 0.3, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 525.5, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 9.0 + }, + { + "DateTime": "2025-09-12t17:00:00+02:00", + "EpochDateTime": 1757689200, + "WeatherIcon": 2, + "IconPhrase": "przewa\u017cnie s\u0142onecznie", + "HasPrecipitation": false, + "IsDaylight": true, + "Temperature": { + "Value": 23.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 22.9, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "RealFeelTemperatureShade": { + "Value": 21.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "WetBulbTemperature": { + "Value": 16.2, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 20.1, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 11.7, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 238, + "Localized": "WSW", + "English": "WSW" + } + }, + "WindGust": { + "Speed": { + "Value": 22.2, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 48, + "IndoorRelativeHumidity": 48, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 2, + "UVIndexFloat": 1.7, + "UVIndexText": "niskie", + "PrecipitationProbability": 1, + "ThunderstormProbability": 0, + "RainProbability": 1, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 17, + "Evapotranspiration": { + "Value": 0.3, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 386.6, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 9.0 + }, + { + "DateTime": "2025-09-12t18:00:00+02:00", + "EpochDateTime": 1757692800, + "WeatherIcon": 2, + "IconPhrase": "przewa\u017cnie s\u0142onecznie", + "HasPrecipitation": false, + "IsDaylight": true, + "Temperature": { + "Value": 21.3, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 20.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "RealFeelTemperatureShade": { + "Value": 20.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "WetBulbTemperature": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 19.1, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.3, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 232, + "Localized": "SW", + "English": "SW" + } + }, + "WindGust": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 56, + "IndoorRelativeHumidity": 56, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 1, + "UVIndexFloat": 1.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 1, + "ThunderstormProbability": 0, + "RainProbability": 1, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 23, + "Evapotranspiration": { + "Value": 0.3, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 224.7, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 7.0 + }, + { + "DateTime": "2025-09-12t19:00:00+02:00", + "EpochDateTime": 1757696400, + "WeatherIcon": 2, + "IconPhrase": "przewa\u017cnie s\u0142onecznie", + "HasPrecipitation": false, + "IsDaylight": true, + "Temperature": { + "Value": 19.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 18.2, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "RealFeelTemperatureShade": { + "Value": 18.2, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "WetBulbTemperature": { + "Value": 15.4, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 17.9, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 224, + "Localized": "SW", + "English": "SW" + } + }, + "WindGust": { + "Speed": { + "Value": 16.7, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 62, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.2, + "UVIndexText": "niskie", + "PrecipitationProbability": 2, + "ThunderstormProbability": 0, + "RainProbability": 2, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 29, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 52.2, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 2.0 + }, + { + "DateTime": "2025-09-12t20:00:00+02:00", + "EpochDateTime": 1757700000, + "WeatherIcon": 35, + "IconPhrase": "zachmurzenie umiarkowane", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 17.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "RealFeelTemperatureShade": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17, + "Phrase": "Przyjemnie" + }, + "WetBulbTemperature": { + "Value": 14.6, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.1, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 219, + "Localized": "SW", + "English": "SW" + } + }, + "WindGust": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 69, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 3, + "ThunderstormProbability": 0, + "RainProbability": 3, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 34, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-12t21:00:00+02:00", + "EpochDateTime": 1757703600, + "WeatherIcon": 35, + "IconPhrase": "zachmurzenie umiarkowane", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 14.9, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 14.9, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.7, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 15.6, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 11.9, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 230, + "Localized": "SW", + "English": "SW" + } + }, + "WindGust": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 77, + "IndoorRelativeHumidity": 59, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 3, + "ThunderstormProbability": 0, + "RainProbability": 3, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 30, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-12t22:00:00+02:00", + "EpochDateTime": 1757707200, + "WeatherIcon": 34, + "IconPhrase": "przewa\u017cnie bezchmurnie", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 14.6, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.8, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.8, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.3, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 14.9, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.0, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 259, + "Localized": "W", + "English": "W" + } + }, + "WindGust": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 84, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 3, + "ThunderstormProbability": 0, + "RainProbability": 3, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 26, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-12t23:00:00+02:00", + "EpochDateTime": 1757710800, + "WeatherIcon": 34, + "IconPhrase": "przewa\u017cnie bezchmurnie", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 14.4, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.8, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.8, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.3, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 14.9, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 272, + "Localized": "W", + "English": "W" + } + }, + "WindGust": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 86, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 16.1, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 4, + "ThunderstormProbability": 0, + "RainProbability": 4, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 22, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-13t00:00:00+02:00", + "EpochDateTime": 1757714400, + "WeatherIcon": 35, + "IconPhrase": "zachmurzenie umiarkowane", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 13.9, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.5, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.5, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.0, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 14.5, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 265, + "Localized": "W", + "English": "W" + } + }, + "WindGust": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 89, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 11.3, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 4, + "ThunderstormProbability": 0, + "RainProbability": 4, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 48, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-13t01:00:00+02:00", + "EpochDateTime": 1757718000, + "WeatherIcon": 36, + "IconPhrase": "przej\u015bciowe zachmurzenia", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 13.6, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.2, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.2, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.0, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 14.0, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.3, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 256, + "Localized": "WSW", + "English": "WSW" + } + }, + "WindGust": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 91, + "IndoorRelativeHumidity": 61, + "Visibility": { + "Value": 11.3, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 4, + "ThunderstormProbability": 0, + "RainProbability": 4, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 74, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-13t02:00:00+02:00", + "EpochDateTime": 1757721600, + "WeatherIcon": 7, + "IconPhrase": "pochmurno", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 13.9, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.5, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.5, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.1, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 13.3, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.3, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 244, + "Localized": "WSW", + "English": "WSW" + } + }, + "WindGust": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 90, + "IndoorRelativeHumidity": 61, + "Visibility": { + "Value": 9.7, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 9144.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 5, + "ThunderstormProbability": 0, + "RainProbability": 5, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 100, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + }, + { + "DateTime": "2025-09-13t03:00:00+02:00", + "EpochDateTime": 1757725200, + "WeatherIcon": 7, + "IconPhrase": "pochmurno", + "HasPrecipitation": false, + "IsDaylight": false, + "Temperature": { + "Value": 14.0, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperature": { + "Value": 13.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "RealFeelTemperatureShade": { + "Value": 13.6, + "Unit": "C", + "UnitType": 17, + "Phrase": "Ch\u0142odno" + }, + "WetBulbTemperature": { + "Value": 13.0, + "Unit": "C", + "UnitType": 17 + }, + "WetBulbGlobeTemperature": { + "Value": 13.4, + "Unit": "C", + "UnitType": 17 + }, + "DewPoint": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "Wind": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 229, + "Localized": "SW", + "English": "SW" + } + }, + "WindGust": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + } + }, + "RelativeHumidity": 89, + "IndoorRelativeHumidity": 60, + "Visibility": { + "Value": 9.7, + "Unit": "km", + "UnitType": 6 + }, + "Ceiling": { + "Value": 7376.0, + "Unit": "m", + "UnitType": 5 + }, + "UVIndex": 0, + "UVIndexFloat": 0.0, + "UVIndexText": "niskie", + "PrecipitationProbability": 5, + "ThunderstormProbability": 0, + "RainProbability": 5, + "SnowProbability": 0, + "IceProbability": 0, + "TotalLiquid": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Rain": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "Snow": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "Ice": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "CloudCover": 98, + "Evapotranspiration": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SolarIrradiance": { + "Value": 0.0, + "Unit": "W/m\u00b2", + "UnitType": 33 + }, + "AccuLumenBrightnessIndex": 0.0 + } +] diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 254667d7809d8..ae17c76511c3b 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_forecast_service[get_forecasts] +# name: test_forecast_service[daily] dict({ 'weather.home': dict({ 'forecast': list([ @@ -82,6 +82,182 @@ }), }) # --- +# name: test_forecast_service[hourly] + dict({ + 'weather.home': dict({ + 'forecast': list([ + dict({ + 'apparent_temperature': 22.6, + 'cloud_coverage': 13, + 'condition': 'sunny', + 'datetime': '2025-09-12T14:00:00+00:00', + 'humidity': 48, + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 22.5, + 'uv_index': 2, + 'wind_bearing': 239, + 'wind_gust_speed': 24.1, + 'wind_speed': 14.8, + }), + dict({ + 'apparent_temperature': 22.9, + 'cloud_coverage': 17, + 'condition': 'sunny', + 'datetime': '2025-09-12T15:00:00+00:00', + 'humidity': 48, + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 23.1, + 'uv_index': 2, + 'wind_bearing': 238, + 'wind_gust_speed': 22.2, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 20.6, + 'cloud_coverage': 23, + 'condition': 'sunny', + 'datetime': '2025-09-12T16:00:00+00:00', + 'humidity': 56, + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 21.3, + 'uv_index': 1, + 'wind_bearing': 232, + 'wind_gust_speed': 18.5, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 18.2, + 'cloud_coverage': 29, + 'condition': 'sunny', + 'datetime': '2025-09-12T17:00:00+00:00', + 'humidity': 62, + 'precipitation': 0.0, + 'precipitation_probability': 2, + 'temperature': 19.5, + 'uv_index': 0, + 'wind_bearing': 224, + 'wind_gust_speed': 16.7, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 16.7, + 'cloud_coverage': 34, + 'condition': 'partlycloudy', + 'datetime': '2025-09-12T18:00:00+00:00', + 'humidity': 69, + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'temperature': 17.7, + 'uv_index': 0, + 'wind_bearing': 219, + 'wind_gust_speed': 14.8, + 'wind_speed': 11.1, + }), + dict({ + 'apparent_temperature': 14.9, + 'cloud_coverage': 30, + 'condition': 'partlycloudy', + 'datetime': '2025-09-12T19:00:00+00:00', + 'humidity': 77, + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'temperature': 15.8, + 'uv_index': 0, + 'wind_bearing': 230, + 'wind_gust_speed': 13.0, + 'wind_speed': 11.1, + }), + dict({ + 'apparent_temperature': 13.8, + 'cloud_coverage': 26, + 'condition': 'clear-night', + 'datetime': '2025-09-12T20:00:00+00:00', + 'humidity': 84, + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'temperature': 14.6, + 'uv_index': 0, + 'wind_bearing': 259, + 'wind_gust_speed': 13.0, + 'wind_speed': 9.3, + }), + dict({ + 'apparent_temperature': 13.8, + 'cloud_coverage': 22, + 'condition': 'clear-night', + 'datetime': '2025-09-12T21:00:00+00:00', + 'humidity': 86, + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 272, + 'wind_gust_speed': 13.0, + 'wind_speed': 9.3, + }), + dict({ + 'apparent_temperature': 13.5, + 'cloud_coverage': 48, + 'condition': 'partlycloudy', + 'datetime': '2025-09-12T22:00:00+00:00', + 'humidity': 89, + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'temperature': 13.9, + 'uv_index': 0, + 'wind_bearing': 265, + 'wind_gust_speed': 13.0, + 'wind_speed': 7.4, + }), + dict({ + 'apparent_temperature': 13.2, + 'cloud_coverage': 74, + 'condition': 'partlycloudy', + 'datetime': '2025-09-12T23:00:00+00:00', + 'humidity': 91, + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'temperature': 13.6, + 'uv_index': 0, + 'wind_bearing': 256, + 'wind_gust_speed': 11.1, + 'wind_speed': 7.4, + }), + dict({ + 'apparent_temperature': 13.5, + 'cloud_coverage': 100, + 'condition': 'cloudy', + 'datetime': '2025-09-13T00:00:00+00:00', + 'humidity': 90, + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'temperature': 13.9, + 'uv_index': 0, + 'wind_bearing': 244, + 'wind_gust_speed': 11.1, + 'wind_speed': 7.4, + }), + dict({ + 'apparent_temperature': 13.6, + 'cloud_coverage': 98, + 'condition': 'cloudy', + 'datetime': '2025-09-13T01:00:00+00:00', + 'humidity': 89, + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 229, + 'wind_gust_speed': 9.3, + 'wind_speed': 7.4, + }), + ]), + }), + }) +# --- # name: test_forecast_subscription list([ dict({ @@ -269,7 +445,7 @@ 'platform': 'accuweather', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '0123456', 'unit_of_measurement': None, @@ -287,7 +463,7 @@ 'precipitation_unit': , 'pressure': 1012.0, 'pressure_unit': , - 'supported_features': , + 'supported_features': , 'temperature': 22.6, 'temperature_unit': , 'uv_index': 6, diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index a23b09fec29b8..7e163e40d8327 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -107,24 +107,24 @@ async def test_unsupported_condition_icon_data( @pytest.mark.parametrize( - ("service"), - [SERVICE_GET_FORECASTS], + ("forecast_type"), + ["daily", "hourly"], ) async def test_forecast_service( hass: HomeAssistant, snapshot: SnapshotAssertion, mock_accuweather_client: AsyncMock, - service: str, + forecast_type: str, ) -> None: """Test multiple forecast.""" await init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, - service, + SERVICE_GET_FORECASTS, { "entity_id": "weather.home", - "type": "daily", + "type": forecast_type, }, blocking=True, return_response=True, diff --git a/tests/components/ai_task/test_init.py b/tests/components/ai_task/test_init.py index e89e4cea67072..5c6465936d91f 100644 --- a/tests/components/ai_task/test_init.py +++ b/tests/components/ai_task/test_init.py @@ -316,7 +316,7 @@ async def test_generate_image_service( assert "image_data" not in result assert result["media_source_id"].startswith("media-source://ai_task/images/") - assert result["url"].startswith("http://10.10.10.10:8123/api/ai_task/images/") + assert result["url"].startswith("/api/ai_task/images/") assert result["mime_type"] == "image/png" assert result["model"] == "mock_model" assert result["revised_prompt"] == "mock_revised_prompt" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index e26e5234f1c27..bc147839c2fed 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -30,6 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: mock_door.status = "closed" mock_door.link_status = "connected" mock_door.battery_level = 100 + mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}" mock_client = AsyncMock() mock_client.get_doors.return_value = [mock_door] @@ -80,6 +81,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: mock_door.status = "closed" mock_door.link_status = "connected" mock_door.battery_level = 100 + mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}" # Mock client mock_client = AsyncMock() diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index efc05772a9aec..2588f61177f5f 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -21,7 +21,7 @@ DEFAULT_PORT, DOMAIN, ) -from homeassistant.components.androidtv.media_player import ( +from homeassistant.components.androidtv.services import ( ATTR_DEVICE_PATH, ATTR_LOCAL_PATH, SERVICE_ADB_COMMAND, diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index 0efeac0e45c9f..ac18d4e4277db 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -2,73 +2,68 @@ from __future__ import annotations -from collections import OrderedDict from typing import Final from homeassistant.const import CONF_HOST, CONF_PORT CONF_DATA: Final = {CONF_HOST: "test", CONF_PORT: 1234} -MOCK_STATUS: Final = OrderedDict( - [ - ("APC", "001,038,0985"), - ("DATE", "1970-01-01 00:00:00 0000"), - ("VERSION", "3.14.14 (31 May 2016) unknown"), - ("CABLE", "USB Cable"), - ("DRIVER", "USB UPS Driver"), - ("UPSMODE", "Stand Alone"), - ("UPSNAME", "MyUPS"), - ("MODEL", "Back-UPS ES 600"), - ("STATUS", "ONLINE"), - ("LINEV", "124.0 Volts"), - ("LOADPCT", "14.0 Percent"), - ("BCHARGE", "100.0 Percent"), - ("TIMELEFT", "51.0 Minutes"), - ("NOMAPNT", "60.0 VA"), - ("ITEMP", "34.6 C Internal"), - ("MBATTCHG", "5 Percent"), - ("MINTIMEL", "3 Minutes"), - ("MAXTIME", "0 Seconds"), - ("SENSE", "Medium"), - ("LOTRANS", "92.0 Volts"), - ("HITRANS", "139.0 Volts"), - ("ALARMDEL", "30 Seconds"), - ("BATTV", "13.7 Volts"), - ("OUTCURNT", "0.88 Amps"), - ("LASTXFER", "Automatic or explicit self test"), - ("NUMXFERS", "1"), - ("XONBATT", "1970-01-01 00:00:00 0000"), - ("TONBATT", "0 Seconds"), - ("CUMONBATT", "8 Seconds"), - ("XOFFBATT", "1970-01-01 00:00:00 0000"), - ("LASTSTEST", "1970-01-01 00:00:00 0000"), - ("SELFTEST", "NO"), - ("STESTI", "7 days"), - ("STATFLAG", "0x05000008"), - ("SERIALNO", "XXXXXXXXXXXX"), - ("BATTDATE", "1970-01-01"), - ("NOMINV", "120 Volts"), - ("NOMBATTV", "12.0 Volts"), - ("NOMPOWER", "330 Watts"), - ("FIRMWARE", "928.a8 .D USB FW:a8"), - ("END APC", "1970-01-01 00:00:00 0000"), - ] -) +MOCK_STATUS: Final = { + "APC": "001,038,0985", + "DATE": "1970-01-01 00:00:00 0000", + "VERSION": "3.14.14 (31 May 2016) unknown", + "CABLE": "USB Cable", + "DRIVER": "USB UPS Driver", + "UPSMODE": "Stand Alone", + "UPSNAME": "MyUPS", + "MODEL": "Back-UPS ES 600", + "STATUS": "ONLINE", + "LINEV": "124.0 Volts", + "LOADPCT": "14.0 Percent", + "BCHARGE": "100.0 Percent", + "TIMELEFT": "51.0 Minutes", + "NOMAPNT": "60.0 VA", + "ITEMP": "34.6 C Internal", + "MBATTCHG": "5 Percent", + "MINTIMEL": "3 Minutes", + "MAXTIME": "0 Seconds", + "SENSE": "Medium", + "LOTRANS": "92.0 Volts", + "HITRANS": "139.0 Volts", + "ALARMDEL": "30 Seconds", + "BATTV": "13.7 Volts", + "OUTCURNT": "0.88 Amps", + "LASTXFER": "Automatic or explicit self test", + "NUMXFERS": "1", + "XONBATT": "1970-01-01 00:00:00 0000", + "TONBATT": "0 Seconds", + "CUMONBATT": "8 Seconds", + "XOFFBATT": "1970-01-01 00:00:00 0000", + "LASTSTEST": "1970-01-01 00:00:00 0000", + "SELFTEST": "NO", + "STESTI": "7 days", + "STATFLAG": "0x05000008", + "SERIALNO": "XXXXXXXXXXXX", + "BATTDATE": "1970-01-01", + "NOMINV": "120 Volts", + "NOMBATTV": "12.0 Volts", + "NOMPOWER": "330 Watts", + "FIRMWARE": "928.a8 .D USB FW:a8", + "END APC": "1970-01-01 00:00:00 0000", +} # Minimal status adapted from http://www.apcupsd.org/manual/manual.html#apcaccess-test. # Most importantly, the "MODEL" and "SERIALNO" fields are removed to test the ability # of the integration to handle such cases. -MOCK_MINIMAL_STATUS: Final = OrderedDict( - [ - ("APC", "001,012,0319"), - ("DATE", "1970-01-01 00:00:00 0000"), - ("RELEASE", "3.8.5"), - ("CABLE", "APC Cable 940-0128A"), - ("UPSMODE", "Stand Alone"), - ("STARTTIME", "1970-01-01 00:00:00 0000"), - ("LINEFAIL", "OK"), - ("BATTSTAT", "OK"), - ("STATFLAG", "0x008"), - ("END APC", "1970-01-01 00:00:00 0000"), - ] -) +MOCK_MINIMAL_STATUS: Final = { + "APC": "001,012,0319", + "DATE": "1970-01-01 00:00:00 0000", + "RELEASE": "3.8.5", + "CABLE": "APC Cable 940-0128A", + "UPSMODE": "Stand Alone", + "STARTTIME": "1970-01-01 00:00:00 0000", + "LINEFAIL": "OK", + "BATTSTAT": "OK", + "STATFLAG": "0x008", + "END APC": "1970-01-01 00:00:00 0000", +} diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 56ca8bde0ba35..5e77b7e929140 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -76,6 +76,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", @@ -177,6 +178,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'test', 'language': 'en-US', 'tts_input': "Sorry, I couldn't understand that", @@ -278,6 +280,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'test', 'language': 'en-US', 'tts_input': "Sorry, I couldn't understand that", @@ -403,6 +406,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index 7a51eddf8d673..e92f3aec3fb48 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -131,6 +131,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': 'hello, how are you?', @@ -365,6 +366,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "hello, how are you? I'm doing well, thank you. What about you?!", @@ -595,6 +597,7 @@ }), dict({ 'data': dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "I'm doing well, thank you.", diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 5e0d915a77e19..5b5ed44e24d9f 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -73,6 +73,7 @@ # --- # name: test_audio_pipeline.5 dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", @@ -166,6 +167,7 @@ # --- # name: test_audio_pipeline_debug.5 dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", @@ -271,6 +273,7 @@ # --- # name: test_audio_pipeline_with_enhancements.5 dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", @@ -386,6 +389,7 @@ # --- # name: test_audio_pipeline_with_wake_word_no_timeout.7 dict({ + 'acknowledge_override': False, 'engine': 'tts.test', 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 7523412236894..fe82f693fde1a 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -16,13 +16,14 @@ stt, tts, ) -from homeassistant.components.assist_pipeline.const import DOMAIN +from homeassistant.components.assist_pipeline.const import ACKNOWLEDGE_PATH, DOMAIN from homeassistant.components.assist_pipeline.pipeline import ( STORAGE_KEY, STORAGE_VERSION, STORAGE_VERSION_MINOR, Pipeline, PipelineData, + PipelineEventType, PipelineStorageCollection, PipelineStore, _async_local_fallback_intent_filter, @@ -31,9 +32,16 @@ async_get_pipelines, async_update_pipeline, ) -from homeassistant.const import MATCH_ALL +from homeassistant.const import ATTR_FRIENDLY_NAME, MATCH_ALL from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import chat_session, intent, llm +from homeassistant.helpers import ( + area_registry as ar, + chat_session, + device_registry as dr, + entity_registry as er, + intent, + llm, +) from homeassistant.setup import async_setup_component from . import MANY_LANGUAGES, process_events @@ -46,7 +54,7 @@ make_10ms_chunk, ) -from tests.common import flush_store +from tests.common import MockConfigEntry, async_mock_service, flush_store from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -1787,3 +1795,296 @@ async def stream_llm_response(): assert "".join(received_tts) == chunk_text assert process_events(events) == snapshot + + +async def test_acknowledge( + hass: HomeAssistant, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + mock_chat_session: chat_session.ChatSession, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that acknowledge sound is played when targets are in the same area.""" + area_1 = area_registry.async_get_or_create("area_1") + + light_1 = entity_registry.async_get_or_create("light", "demo", "1234") + hass.states.async_set(light_1.entity_id, "off", {ATTR_FRIENDLY_NAME: "light 1"}) + light_1 = entity_registry.async_update_entity(light_1.entity_id, area_id=area_1.id) + + light_2 = entity_registry.async_get_or_create("light", "demo", "5678") + hass.states.async_set(light_2.entity_id, "off", {ATTR_FRIENDLY_NAME: "light 2"}) + light_2 = entity_registry.async_update_entity(light_2.entity_id, area_id=area_1.id) + + entry = MockConfigEntry() + entry.add_to_hass(hass) + satellite = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, + ) + device_registry.async_update_device(satellite.id, area_id=area_1.id) + + events: list[assist_pipeline.PipelineEvent] = [] + turn_on = async_mock_service(hass, "light", "turn_on") + + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + async def _run(text: str) -> None: + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input=text, + session=mock_chat_session, + device_id=satellite.id, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + await pipeline_input.execute() + + with patch( + "homeassistant.components.assist_pipeline.PipelineRun.text_to_speech" + ) as text_to_speech: + + def _reset() -> None: + events.clear() + text_to_speech.reset_mock() + turn_on.clear() + + # 1. All targets in same area + await _run("turn on the lights") + + # Acknowledgment sound should be played (same area) + text_to_speech.assert_called_once() + assert ( + text_to_speech.call_args.kwargs["override_media_path"] == ACKNOWLEDGE_PATH + ) + assert len(turn_on) == 2 + + # 2. One light in a different area + area_2 = area_registry.async_get_or_create("area_2") + light_2 = entity_registry.async_update_entity( + light_2.entity_id, area_id=area_2.id + ) + + _reset() + await _run("turn on light 2") + + # Acknowledgment sound should be not played (different area) + text_to_speech.assert_called_once() + assert text_to_speech.call_args.kwargs.get("override_media_path") is None + assert len(turn_on) == 1 + + # Restore + light_2 = entity_registry.async_update_entity( + light_2.entity_id, area_id=area_1.id + ) + + # 3. Remove satellite device area + device_registry.async_update_device(satellite.id, area_id=None) + + _reset() + await _run("turn on light 1") + + # Acknowledgment sound should be not played (no satellite area) + text_to_speech.assert_called_once() + assert text_to_speech.call_args.kwargs.get("override_media_path") is None + assert len(turn_on) == 1 + + # Restore + device_registry.async_update_device(satellite.id, area_id=area_1.id) + + # 4. Check device area instead of entity area + light_device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-5678")}, + ) + device_registry.async_update_device(light_device.id, area_id=area_1.id) + light_2 = entity_registry.async_update_entity( + light_2.entity_id, area_id=None, device_id=light_device.id + ) + + _reset() + await _run("turn on the lights") + + # Acknowledgment sound should be played (same area) + text_to_speech.assert_called_once() + assert ( + text_to_speech.call_args.kwargs["override_media_path"] == ACKNOWLEDGE_PATH + ) + assert len(turn_on) == 2 + + # 5. Move device to different area + device_registry.async_update_device(light_device.id, area_id=area_2.id) + + _reset() + await _run("turn on light 2") + + # Acknowledgment sound should be not played (different device area) + text_to_speech.assert_called_once() + assert text_to_speech.call_args.kwargs.get("override_media_path") is None + assert len(turn_on) == 1 + + # 6. No device or area + light_2 = entity_registry.async_update_entity( + light_2.entity_id, area_id=None, device_id=None + ) + + _reset() + await _run("turn on light 2") + + # Acknowledgment sound should be not played (no area) + text_to_speech.assert_called_once() + assert text_to_speech.call_args.kwargs.get("override_media_path") is None + assert len(turn_on) == 1 + + # 7. Not in entity registry + hass.states.async_set("light.light_3", "off", {ATTR_FRIENDLY_NAME: "light 3"}) + + _reset() + await _run("turn on light 3") + + # Acknowledgment sound should be not played (not in entity registry) + text_to_speech.assert_called_once() + assert text_to_speech.call_args.kwargs.get("override_media_path") is None + assert len(turn_on) == 1 + + # Check TTS event + events.clear() + await _run("turn on light 1") + + has_acknowledge_override: bool | None = None + for event in events: + if event.type == PipelineEventType.TTS_START: + assert event.data + has_acknowledge_override = event.data["acknowledge_override"] + break + + assert has_acknowledge_override + + +async def test_acknowledge_other_agents( + hass: HomeAssistant, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + mock_chat_session: chat_session.ChatSession, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that acknowledge sound is only played when intents are processed locally for other agents.""" + area_1 = area_registry.async_get_or_create("area_1") + + light_1 = entity_registry.async_get_or_create("light", "demo", "1234") + hass.states.async_set(light_1.entity_id, "off", {ATTR_FRIENDLY_NAME: "light 1"}) + light_1 = entity_registry.async_update_entity(light_1.entity_id, area_id=area_1.id) + + light_2 = entity_registry.async_get_or_create("light", "demo", "5678") + hass.states.async_set(light_2.entity_id, "off", {ATTR_FRIENDLY_NAME: "light 2"}) + light_2 = entity_registry.async_update_entity(light_2.entity_id, area_id=area_1.id) + + entry = MockConfigEntry() + entry.add_to_hass(hass) + satellite = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, + ) + device_registry.async_update_device(satellite.id, area_id=area_1.id) + + events: list[assist_pipeline.PipelineEvent] = [] + async_mock_service(hass, "light", "turn_on") + + pipeline_store = pipeline_data.pipeline_store + pipeline = await pipeline_store.async_create_item( + { + "name": "Test 1", + "language": "en-US", + "conversation_engine": "test agent", + "conversation_language": "en-US", + "tts_engine": "test tts", + "tts_language": "en-US", + "tts_voice": "test voice", + "stt_engine": "test stt", + "stt_language": "en-US", + "wake_word_entity": None, + "wake_word_id": None, + "prefer_local_intents": True, + } + ) + + with ( + patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), + ), + patch( + "homeassistant.components.assist_pipeline.PipelineRun.prepare_text_to_speech" + ), + patch( + "homeassistant.components.assist_pipeline.PipelineRun.text_to_speech" + ) as text_to_speech, + patch( + "homeassistant.components.conversation.async_converse", return_value=None + ) as async_converse, + patch( + "homeassistant.components.assist_pipeline.PipelineRun._get_all_targets_in_satellite_area" + ) as get_all_targets_in_satellite_area, + ): + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="turn on the lights", + session=mock_chat_session, + device_id=satellite.id, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + await pipeline_input.execute() + + # Processed locally + async_converse.assert_not_called() + + # Not processed locally + text_to_speech.reset_mock() + get_all_targets_in_satellite_area.reset_mock() + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="not processed locally", + session=mock_chat_session, + device_id=satellite.id, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + await pipeline_input.execute() + + # The acknowledgment should not have even been checked for because the + # default agent didn't handle the intent. + text_to_speech.assert_not_called() + async_converse.assert_called_once() + get_all_targets_in_satellite_area.assert_not_called() diff --git a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr index c3c3b8f185d0f..8236540654d11 100644 --- a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr @@ -216,7 +216,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -254,6 +256,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Device Name Exhaust air temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -269,7 +272,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -307,6 +312,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Device Name Extract air temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -482,7 +488,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -520,6 +528,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Device Name Outside air temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -591,7 +600,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -629,6 +640,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Device Name Room temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -748,7 +760,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -786,6 +800,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Device Name Supply air temperature', + 'state_class': , 'unit_of_measurement': , }), 'context': , diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index ad80c7cb94ab7..a86c782a2ebb9 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -39,8 +39,9 @@ "cleanCycleWaitTime": 15, "isKeypadLockout": False, "nightLightMode": "OFF", - "nightLightBrightness": 85, + "nightLightBrightness": 50, "isPanelSleepMode": False, + "panelBrightnessHigh": 50, "panelSleepTime": 0, "panelWakeTime": 0, "weekdaySleepModeEnabled": { diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index aa67db23d8966..5075b5d5efd7e 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -39,6 +39,7 @@ def create_mock_robot( robot = LitterRobot4(data={**ROBOT_4_DATA, **robot_data}, account=account) elif feeder: robot = FeederRobot(data={**FEEDER_ROBOT_DATA, **robot_data}, account=account) + robot.set_gravity_mode = AsyncMock(side_effect=side_effect) else: robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account) robot.start_cleaning = AsyncMock(side_effect=side_effect) diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index b4902a56e632f..873e65b33ffda 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -19,7 +19,6 @@ from .conftest import setup_integration SELECT_ENTITY_ID = "select.test_clean_cycle_wait_time_minutes" -PANEL_BRIGHTNESS_ENTITY_ID = "select.test_panel_brightness" async def test_wait_time_select( @@ -69,26 +68,38 @@ async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> No assert not mock_account.robots[0].set_wait_time.called -async def test_panel_brightness_select( +@pytest.mark.parametrize( + ("entity_id", "initial_value", "robot_command"), + [ + ("select.test_globe_brightness", "medium", "set_night_light_brightness"), + ("select.test_globe_light", "off", "set_night_light_mode"), + ("select.test_panel_brightness", "medium", "set_panel_brightness"), + ], +) +async def test_litterrobot_4_select( hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock, entity_registry: er.EntityRegistry, + entity_id: str, + initial_value: str, + robot_command: str, ) -> None: - """Tests the wait time select entity.""" + """Tests a Litter-Robot 4 select entity.""" await setup_integration(hass, mock_account_with_litterrobot_4, SELECT_DOMAIN) - select = hass.states.get(PANEL_BRIGHTNESS_ENTITY_ID) + select = hass.states.get(entity_id) assert select assert len(select.attributes[ATTR_OPTIONS]) == 3 + assert select.state == initial_value - entity_entry = entity_registry.async_get(PANEL_BRIGHTNESS_ENTITY_ID) + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.entity_category is EntityCategory.CONFIG - data = {ATTR_ENTITY_ID: PANEL_BRIGHTNESS_ENTITY_ID} + data = {ATTR_ENTITY_ID: entity_id} robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] - robot.set_panel_brightness = AsyncMock(return_value=True) + setattr(robot, robot_command, AsyncMock(return_value=True)) for count, option in enumerate(select.attributes[ATTR_OPTIONS]): data[ATTR_OPTION] = option @@ -100,4 +111,4 @@ async def test_panel_brightness_select( blocking=True, ) - assert robot.set_panel_brightness.call_count == count + 1 + assert getattr(robot, robot_command).call_count == count + 1 diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index d81c02bee49a8..a1ccddc79d177 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from pylitterbot import Robot +from pylitterbot import FeederRobot, Robot import pytest from homeassistant.components.switch import ( @@ -66,3 +66,27 @@ async def test_on_off_commands( assert getattr(robot, robot_command).call_count == count + 1 assert (state := hass.states.get(entity_id)) assert state.state == new_state + + +async def test_feeder_robot_switch( + hass: HomeAssistant, mock_account_with_feederrobot: MagicMock +) -> None: + """Tests Feeder-Robot switches.""" + await setup_integration(hass, mock_account_with_feederrobot, PLATFORM_DOMAIN) + robot: FeederRobot = mock_account_with_feederrobot.robots[0] + + gravity_mode_switch = "switch.test_gravity_mode" + + switch = hass.states.get(gravity_mode_switch) + assert switch.state == STATE_OFF + + data = {ATTR_ENTITY_ID: gravity_mode_switch} + + services = ((SERVICE_TURN_ON, STATE_ON, True), (SERVICE_TURN_OFF, STATE_OFF, False)) + for count, (service, new_state, new_value) in enumerate(services): + await hass.services.async_call(PLATFORM_DOMAIN, service, data, blocking=True) + robot._update_data({"state": {"info": {"gravity": new_value}}}, partial=True) + + assert robot.set_gravity_mode.call_count == count + 1 + assert (state := hass.states.get(gravity_mode_switch)) + assert state.state == new_state diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index a7a70043d94eb..c7f96e3aa2afd 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.min_max.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, SERVICE_RELOAD, @@ -59,6 +60,7 @@ async def test_default_name_sensor(hass: HomeAssistant) -> None: assert str(float(MIN_VALUE)) == state.state assert entity_ids[2] == state.attributes.get("min_entity_id") + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids async def test_min_sensor( diff --git a/tests/components/roborock/test_coordinator.py b/tests/components/roborock/test_coordinator.py index dec4e0a62d40f..22efddf5817fb 100644 --- a/tests/components/roborock/test_coordinator.py +++ b/tests/components/roborock/test_coordinator.py @@ -6,13 +6,16 @@ import pytest from roborock.exceptions import RoborockException +from vacuum_map_parser_base.config.color import SupportedColor from homeassistant.components.roborock.const import ( + CONF_SHOW_BACKGROUND, V1_CLOUD_IN_CLEANING_INTERVAL, V1_CLOUD_NOT_CLEANING_INTERVAL, V1_LOCAL_IN_CLEANING_INTERVAL, V1_LOCAL_NOT_CLEANING_INTERVAL, ) +from homeassistant.components.roborock.coordinator import RoborockDataUpdateCoordinator from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -73,6 +76,26 @@ async def test_dynamic_cloud_scan_interval( assert hass.states.get("sensor.roborock_s7_maxv_battery").state == "20" +async def test_visible_background( + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture: None, +) -> None: + """Test that a visible background is handled correctly.""" + hass.config_entries.async_update_entry( + mock_roborock_entry, + options={ + CONF_SHOW_BACKGROUND: True, + }, + ) + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + await hass.async_block_till_done() + coordinator: RoborockDataUpdateCoordinator = mock_roborock_entry.runtime_data.v1[0] + assert coordinator.map_parser._palette.get_color( # pylint: disable=protected-access + SupportedColor.MAP_OUTSIDE + ) != (0, 0, 0, 0) + + @pytest.mark.parametrize( ("interval", "in_cleaning"), [ diff --git a/tests/components/shelly/snapshots/test_button.ambr b/tests/components/shelly/snapshots/test_button.ambr index 09c2c5f3d8da8..cd0f88e37972d 100644 --- a/tests/components/shelly/snapshots/test_button.ambr +++ b/tests/components/shelly/snapshots/test_button.ambr @@ -96,3 +96,51 @@ 'state': 'unknown', }) # --- +# name: test_rpc_device_virtual_button[button.test_name_button-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.test_name_button', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Button', + 'platform': 'shelly', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-button:200', + 'unit_of_measurement': None, + }) +# --- +# name: test_rpc_device_virtual_button[button.test_name_button-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test name Button', + }), + 'context': , + 'entity_id': 'button.test_name_button', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 8d355098463c4..3bf70f20f2e58 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,5 +1,6 @@ """Tests for Shelly button platform.""" +from copy import deepcopy from unittest.mock import Mock from aioshelly.const import MODEL_BLU_GATEWAY_G3 @@ -13,9 +14,10 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from . import init_integration +from . import init_integration, register_device, register_entity async def test_block_button( @@ -278,3 +280,65 @@ async def test_rpc_blu_trv_button_auth_error( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_rpc_device_virtual_button( + hass: HomeAssistant, + entity_registry: EntityRegistry, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + snapshot: SnapshotAssertion, +) -> None: + """Test a virtual button for RPC device.""" + config = deepcopy(mock_rpc_device.config) + config["button:200"] = { + "name": "Button", + "meta": {"ui": {"view": "button"}}, + } + monkeypatch.setattr(mock_rpc_device, "config", config) + + status = deepcopy(mock_rpc_device.status) + status["button:200"] = {"value": None} + monkeypatch.setattr(mock_rpc_device, "status", status) + + await init_integration(hass, 3) + entity_id = "button.test_name_button" + + assert (state := hass.states.get(entity_id)) + assert state == snapshot(name=f"{entity_id}-state") + + assert (entry := entity_registry.async_get(entity_id)) + assert entry == snapshot(name=f"{entity_id}-entry") + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_rpc_device.button_trigger.assert_called_once_with(200, "single_push") + + +async def test_rpc_remove_virtual_button_when_orphaned( + hass: HomeAssistant, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + mock_rpc_device: Mock, +) -> None: + """Check whether the virtual button will be removed if it has been removed from the device configuration.""" + config_entry = await init_integration(hass, 3, skip_setup=True) + device_entry = register_device(device_registry, config_entry) + entity_id = register_entity( + hass, + BUTTON_DOMAIN, + "test_name_button_200", + "button:200", + config_entry, + device_id=device_entry.id, + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entry = entity_registry.async_get(entity_id) + assert not entry diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index ff61eda626f75..e4549d9c4a0dc 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -553,6 +553,57 @@ async def test_rpc_click_event( } +async def test_rpc_ignore_virtual_click_event( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_rpc_device: Mock, + events: list[Event], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC virtual click events are ignored as they are triggered by the integration.""" + await init_integration(hass, 2) + + # Generate a virtual button event + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "button:200", + "id": 200, + "event": "single_push", + "ts": 1757358109.89, + } + ], + "ts": 757358109.89, + }, + ) + await hass.async_block_till_done() + + assert len(events) == 0 + + # Generate valid event + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "data": [], + "event": "single_push", + "id": 0, + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + await hass.async_block_till_done() + + assert len(events) == 1 + + async def test_rpc_update_entry_sleep_period( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/tibber/test_services.py b/tests/components/tibber/test_services.py index dc6f5d2789df2..9c9fb86f91717 100644 --- a/tests/components/tibber/test_services.py +++ b/tests/components/tibber/test_services.py @@ -88,24 +88,20 @@ async def test_get_prices( { "start_time": START_TIME.isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, { "start_time": (START_TIME + dt.timedelta(hours=1)).isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, ], "second_home": [ { "start_time": START_TIME.isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, { "start_time": (START_TIME + dt.timedelta(hours=1)).isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, ], } @@ -138,24 +134,20 @@ async def test_get_prices_start_tomorrow( { "start_time": tomorrow.isoformat(), "price": 0.46914, - "level": "VERY_EXPENSIVE", }, { "start_time": (tomorrow + dt.timedelta(hours=1)).isoformat(), "price": 0.46914, - "level": "VERY_EXPENSIVE", }, ], "second_home": [ { "start_time": tomorrow.isoformat(), "price": 0.46914, - "level": "VERY_EXPENSIVE", }, { "start_time": (tomorrow + dt.timedelta(hours=1)).isoformat(), "price": 0.46914, - "level": "VERY_EXPENSIVE", }, ], } @@ -197,24 +189,20 @@ async def test_get_prices_with_timezones( { "start_time": START_TIME.isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, { "start_time": (START_TIME + dt.timedelta(hours=1)).isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, ], "second_home": [ { "start_time": START_TIME.isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, { "start_time": (START_TIME + dt.timedelta(hours=1)).isoformat(), "price": 0.36914, - "level": "VERY_EXPENSIVE", }, ], } diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index a699eb7846ce8..21e558b7192d1 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -208,11 +208,11 @@ async def _create_device(hass: HomeAssistant, mock_device_code: str) -> Customer } device.status = details["status"] for key, value in device.status.items(): - # Some devices to not provide a status_range for all status DPs - dp_type = device.status_range.get(key) - if dp_type is None: - dp_type = device.function[key] - if dp_type.type == "Json": + # Some devices do not provide a status_range for all status DPs + # Others set the type as String in status_range and as Json in function + if ((dp_type := device.status_range.get(key)) and dp_type.type == "Json") or ( + (dp_type := device.function.get(key)) and dp_type.type == "Json" + ): device.status[key] = json_dumps(value) return device diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index a3b0b0b10c8d0..2a3f5687c525d 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -330,7 +330,7 @@ 'labels': set({ }), 'manufacturer': 'Tuya', - 'model': 'YINMIK Water Quality Tester (unsupported)', + 'model': 'YINMIK Water Quality Tester', 'model_id': 'u5xgcpcngk3pfxb4', 'name': 'YINMIK Water Quality Tester', 'name_by_user': None, @@ -2004,7 +2004,7 @@ 'labels': set({ }), 'manufacturer': 'Tuya', - 'model': 'BlissRadia (unsupported)', + 'model': 'BlissRadia ', 'model_id': 'ssimhf6r8kgwepfb', 'name': 'BlissRadia ', 'name_by_user': None, @@ -5755,7 +5755,7 @@ 'labels': set({ }), 'manufacturer': 'Tuya', - 'model': 'Smart White Noise Machine (unsupported)', + 'model': 'Smart White Noise Machine', 'model_id': '45idzfufidgee7ir', 'name': 'Smart White Noise Machine', 'name_by_user': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 54c4b8784d6e8..c8d7556fa11c5 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -345,6 +345,67 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.blissradia-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.blissradia', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.blissradia-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'BlissRadia ', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.blissradia', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[light.cam_garage_indicator_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2957,6 +3018,77 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.smart_white_noise_machine-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.smart_white_noise_machine', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.smart_white_noise_machine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 1003, + 'color_mode': , + 'friendly_name': 'Smart White Noise Machine', + 'hs_color': tuple( + 239.666, + 393.307, + ), + 'rgb_color': tuple( + -748, + -742, + 255, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + -0.03, + -0.215, + ), + }), + 'context': , + 'entity_id': 'light.smart_white_noise_machine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[light.solar_zijpad-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 73dab1877e1d4..15003c65db03d 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -58,6 +58,64 @@ 'state': '1.0', }) # --- +# name: test_platform_setup_and_discovery[number.blissradia_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 5.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.blissradia_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbvolume_set', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.blissradia_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BlissRadia Volume', + 'max': 100.0, + 'min': 5.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.blissradia_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- # name: test_platform_setup_and_discovery[number.boiler_temperature_controller_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2223,6 +2281,64 @@ 'state': '-2.0', }) # --- +# name: test_platform_setup_and_discovery[number.smart_white_noise_machine_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.smart_white_noise_machine_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbvolume_set', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.smart_white_noise_machine_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine Volume', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.smart_white_noise_machine_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) +# --- # name: test_platform_setup_and_discovery[number.sous_vide_cooking_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 1a5061f3b1a0f..ce90522885d65 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -3197,6 +3197,65 @@ 'state': 'level_1', }) # --- +# name: test_platform_setup_and_discovery[select.mesa_up_down-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'up', + 'down', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.mesa_up_down', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Up/Down', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'desk_up_down', + 'unique_id': 'tuya.vpfdskpi8pr8cbtfzjsup_down', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.mesa_up_down-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'mesa Up/Down', + 'options': list([ + 'up', + 'down', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'select.mesa_up_down', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- # name: test_platform_setup_and_discovery[select.mirilla_puerta_anti_flicker-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 464bdd353ec7b..1ec5a6c32310d 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -19011,3 +19011,164 @@ 'state': '231.4', }) # --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.yinmik_water_quality_tester_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.4bxfp3kgncpcgx5uycjzsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'YINMIK Water Quality Tester Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.yinmik_water_quality_tester_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.yinmik_water_quality_tester_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.4bxfp3kgncpcgx5uycjzstemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'YINMIK Water Quality Tester Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yinmik_water_quality_tester_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41.2', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_total_dissolved_solids-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.yinmik_water_quality_tester_total_dissolved_solids', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total dissolved solids', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_dissolved_solids', + 'unique_id': 'tuya.4bxfp3kgncpcgx5uycjzstds_in', + 'unit_of_measurement': 'ppt', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yinmik_water_quality_tester_total_dissolved_solids-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'YINMIK Water Quality Tester Total dissolved solids', + 'state_class': , + 'unit_of_measurement': 'ppt', + }), + 'context': , + 'entity_id': 'sensor.yinmik_water_quality_tester_total_dissolved_solids', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.476', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 1b481daa94572..7df3249aa67d8 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1070,6 +1070,55 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[switch.blissradia_snooze-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.blissradia_snooze', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:alarm-snooze', + 'original_name': 'Snooze', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'snooze', + 'unique_id': 'tuya.bfpewgk8r6fhmissdyzbsnooze', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.blissradia_snooze-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BlissRadia Snooze', + 'icon': 'mdi:alarm-snooze', + }), + 'context': , + 'entity_id': 'switch.blissradia_snooze', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -7403,6 +7452,103 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.smart_white_noise_machine', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine', + }), + 'context': , + 'entity_id': 'switch.smart_white_noise_machine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine_music-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.smart_white_noise_machine_music', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:music', + 'original_name': 'Music', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'music', + 'unique_id': 'tuya.ri7eegdifufzdi54dyzbswitch_music', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_white_noise_machine_music-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart White Noise Machine Music', + 'icon': 'mdi:music', + }), + 'context': , + 'entity_id': 'switch.smart_white_noise_machine_music', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[switch.smoke_alarm_mute-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 01f003327c125..7a404e3d877ca 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -80,7 +80,7 @@ class MockApiResponseKey(str, Enum): def mock_uptimerobot_api_response( - data: dict[str, Any] + data: list[dict[str, Any]] | list[UptimeRobotMonitor] | UptimeRobotAccount | UptimeRobotApiError @@ -115,8 +115,10 @@ async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON - assert hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY).state == STATE_UP + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON + assert (entity := hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_UP assert mock_entry.state is ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py index 3de9b9ec39963..c214a7d15434d 100644 --- a/tests/components/uptimerobot/test_binary_sensor.py +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -26,8 +26,7 @@ async def test_presentation(hass: HomeAssistant) -> None: """Test the presenstation of UptimeRobot binary_sensors.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) - + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) assert entity.state == STATE_ON assert entity.attributes["device_class"] == BinarySensorDeviceClass.CONNECTIVITY assert entity.attributes["attribution"] == ATTRIBUTION @@ -38,7 +37,7 @@ async def test_unavailable_on_update_failure(hass: HomeAssistant) -> None: """Test entity unavailable on update failure.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) assert entity.state == STATE_ON with patch( @@ -48,5 +47,5 @@ async def test_unavailable_on_update_failure(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - entity = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) assert entity.state == STATE_UNAVAILABLE diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 621d9cc27c386..ce6ec7cfcf7c6 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -80,6 +80,7 @@ async def test_user_key_read_only(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM + assert result2["errors"] assert result2["errors"]["base"] == "not_main_key" @@ -107,6 +108,7 @@ async def test_exception_thrown(hass: HomeAssistant, exception, error_key) -> No ) assert result2["type"] is FlowResultType.FORM + assert result2["errors"] assert result2["errors"]["base"] == error_key @@ -125,6 +127,7 @@ async def test_api_error(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) + assert result2["errors"] assert result2["errors"]["base"] == "unknown" assert "test error from API." in caplog.text @@ -227,6 +230,7 @@ async def test_reauthentication_failure( assert result2["step_id"] == "reauth_confirm" assert result2["type"] is FlowResultType.FORM + assert result2["errors"] assert result2["errors"]["base"] == "unknown" @@ -299,6 +303,7 @@ async def test_reauthentication_failure_account_not_matching( assert result2["step_id"] == "reauth_confirm" assert result2["type"] is FlowResultType.FORM + assert result2["errors"] assert result2["errors"]["base"] == "reauth_failed_matching_account" @@ -374,6 +379,7 @@ async def test_reconfigure_failed( ) assert result2["type"] is FlowResultType.FORM + assert result2["errors"] assert result2["errors"]["base"] == "invalid_api_key" new_key = "u0242ac120003-new" diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 435b0737c6db5..d252501aa28bb 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -102,7 +102,7 @@ async def test_reauthentication_trigger_after_setup( """Test reauthentication trigger.""" mock_config_entry = await setup_uptimerobot_integration(hass) - binary_sensor = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) + assert (binary_sensor := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) assert mock_config_entry.state is ConfigEntryState.LOADED assert binary_sensor.state == STATE_ON @@ -115,10 +115,8 @@ async def test_reauthentication_trigger_after_setup( await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() - assert ( - hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state - == STATE_UNAVAILABLE - ) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_UNAVAILABLE assert "Authentication failed while fetching uptimerobot data" in caplog.text @@ -146,9 +144,10 @@ async def test_integration_reload( async_fire_time_changed(hass) await hass.async_block_till_done() - entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert (entry := hass.config_entries.async_get_entry(mock_entry.entry_id)) assert entry.state is ConfigEntryState.LOADED - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON async def test_update_errors( @@ -166,10 +165,8 @@ async def test_update_errors( freezer.tick(COORDINATOR_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert ( - hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state - == STATE_UNAVAILABLE - ) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_UNAVAILABLE with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -178,7 +175,8 @@ async def test_update_errors( freezer.tick(COORDINATOR_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -187,10 +185,8 @@ async def test_update_errors( freezer.tick(COORDINATOR_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert ( - hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state - == STATE_UNAVAILABLE - ) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_UNAVAILABLE assert "Error fetching uptimerobot data: test error from API" in caplog.text @@ -209,7 +205,8 @@ async def test_device_management( assert devices[0].identifiers == {(DOMAIN, "1234")} assert devices[0].name == "Test monitor" - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON assert hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2") is None with patch( @@ -227,10 +224,10 @@ async def test_device_management( assert devices[0].identifiers == {(DOMAIN, "1234")} assert devices[1].identifiers == {(DOMAIN, "12345")} - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON - assert ( - hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2").state == STATE_ON - ) + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON + assert (entity2 := hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2")) + assert entity2.state == STATE_ON with patch( "pyuptimerobot.UptimeRobot.async_get_monitors", @@ -244,5 +241,6 @@ async def test_device_management( assert len(devices) == 1 assert devices[0].identifiers == {(DOMAIN, "1234")} - assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON + assert (entity := hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY)) + assert entity.state == STATE_ON assert hass.states.get(f"{UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY}_2") is None diff --git a/tests/components/uptimerobot/test_sensor.py b/tests/components/uptimerobot/test_sensor.py index 8cee33c1052f5..15e0b0ba1316c 100644 --- a/tests/components/uptimerobot/test_sensor.py +++ b/tests/components/uptimerobot/test_sensor.py @@ -24,8 +24,7 @@ async def test_presentation(hass: HomeAssistant) -> None: """Test the presentation of UptimeRobot sensors.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) - + assert (entity := hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY)) is not None assert entity.state == STATE_UP assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] assert entity.attributes["device_class"] == SensorDeviceClass.ENUM @@ -42,7 +41,7 @@ async def test_unavailable_on_update_failure(hass: HomeAssistant) -> None: """Test entity unavailable on update failure.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY)) is not None assert entity.state == STATE_UP with patch( @@ -52,5 +51,5 @@ async def test_unavailable_on_update_failure(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY)) is not None assert entity.state == STATE_UNAVAILABLE diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py index 48e9da05720b5..a88158ea76558 100644 --- a/tests/components/uptimerobot/test_switch.py +++ b/tests/components/uptimerobot/test_switch.py @@ -33,8 +33,7 @@ async def test_presentation(hass: HomeAssistant) -> None: """Test the presentation of UptimeRobot switches.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) - + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_ON assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] @@ -67,7 +66,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: blocking=True, ) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_OFF @@ -97,7 +96,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: blocking=True, ) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_ON @@ -107,7 +106,7 @@ async def test_authentication_error( """Test authentication error turning switch on/off.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_ON with ( @@ -133,7 +132,7 @@ async def test_action_execution_failure(hass: HomeAssistant) -> None: """Test turning switch on/off failure.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_ON with ( @@ -161,7 +160,7 @@ async def test_switch_api_failure(hass: HomeAssistant) -> None: """Test general exception turning switch on/off.""" await setup_uptimerobot_integration(hass) - entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert (entity := hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY)) is not None assert entity.state == STATE_ON with patch( diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2963db70ad4eb..74a2d15b9ba94 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -333,3 +333,47 @@ def store_event(event): assert len(events) == 2 assert events[1].data["hello"] == "yo2 world" + + +async def test_webhook_template( + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator +) -> None: + """Test triggering with a template webhook.""" + # Set up fake cloud + hass.config.components.add("cloud") + + events = [] + + @callback + def store_event(event): + """Help store events.""" + events.append(event) + + hass.bus.async_listen("test_success", store_event) + + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "webhook", + "webhook_id": "webhook-{{ sqrt(9)|round }}", + "local_only": True, + }, + "action": { + "event": "test_success", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + } + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/webhook-3", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data["hello"] == "yo world"