Merge branch 'dev' into synology_dsm/record-current-IQS

This commit is contained in:
Michael
2025-05-29 18:17:43 +02:00
committed by GitHub
4935 changed files with 272886 additions and 76839 deletions

View File

@ -1,5 +1,6 @@
name: Report an issue with Home Assistant Core name: Report an issue with Home Assistant Core
description: Report an issue with Home Assistant Core. description: Report an issue with Home Assistant Core.
type: Bug
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@ -32,7 +32,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: translations name: translations
@ -324,7 +324,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.8.1 uses: sigstore/cosign-installer@v3.8.2
with: with:
cosign-release: "v2.2.3" cosign-release: "v2.2.3"
@ -457,12 +457,12 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: translations name: translations
@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@ -522,7 +522,7 @@ jobs:
- name: Push Docker image - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push id: push
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@ -531,7 +531,7 @@ jobs:
- name: Generate artifact attestation - name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
with: with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }} subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}

View File

@ -37,10 +37,10 @@ on:
type: boolean type: boolean
env: env:
CACHE_VERSION: 12 CACHE_VERSION: 2
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.4" HA_SHORT_VERSION: "2025.7"
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']" ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -249,7 +249,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -259,7 +259,7 @@ jobs:
with: with:
path: venv path: venv
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Create Python virtual environment - name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
@ -276,7 +276,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Install pre-commit dependencies - name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true' if: steps.cache-precommit.outputs.cache-hit != 'true'
@ -294,7 +294,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -306,7 +306,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
@ -315,7 +315,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff-format - name: Run ruff-format
run: | run: |
@ -334,7 +334,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -346,7 +346,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
@ -355,7 +355,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff - name: Run ruff
run: | run: |
@ -374,7 +374,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -386,7 +386,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
@ -395,7 +395,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Register yamllint problem matcher - name: Register yamllint problem matcher
@ -484,7 +484,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -501,7 +501,7 @@ jobs:
with: with:
path: venv path: venv
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache - name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
@ -509,10 +509,10 @@ jobs:
with: with:
path: ${{ env.UV_CACHE_DIR }} path: ${{ env.UV_CACHE_DIR }}
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }} steps.generate-uv-key.outputs.key }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{ env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}- env.HA_SHORT_VERSION }}-
- name: Install additional OS dependencies - name: Install additional OS dependencies
@ -587,7 +587,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -598,7 +598,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Run hassfest - name: Run hassfest
run: | run: |
@ -620,7 +620,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -631,7 +631,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Run gen_requirements_all.py - name: Run gen_requirements_all.py
run: | run: |
@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Dependency review - name: Dependency review
uses: actions/dependency-review-action@v4.5.0 uses: actions/dependency-review-action@v4.7.1
with: with:
license-check: false # We use our own license audit checks license-check: false # We use our own license audit checks
@ -677,7 +677,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -688,7 +688,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Extract license data - name: Extract license data
run: | run: |
@ -720,7 +720,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -731,7 +731,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher - name: Register pylint problem matcher
run: | run: |
@ -767,7 +767,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -778,7 +778,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher - name: Register pylint problem matcher
run: | run: |
@ -812,7 +812,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -830,17 +830,17 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore mypy cache - name: Restore mypy cache
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.3
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-mypy-key.outputs.key }} steps.generate-mypy-key.outputs.key }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{ env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}- env.HA_SHORT_VERSION }}-
- name: Register mypy problem matcher - name: Register mypy problem matcher
@ -889,7 +889,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -900,7 +900,7 @@ jobs:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Run split_tests.py - name: Run split_tests.py
run: | run: |
@ -944,12 +944,13 @@ jobs:
bluez \ bluez \
ffmpeg \ ffmpeg \
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev \
libxml2-utils
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -959,7 +960,8 @@ jobs:
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
@ -968,7 +970,7 @@ jobs:
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets - name: Download pytest_buckets
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: pytest_buckets name: pytest_buckets
- name: Compile English translations - name: Compile English translations
@ -1019,6 +1021,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
overwrite: true overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact - name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled() if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
@ -1069,12 +1077,13 @@ jobs:
bluez \ bluez \
ffmpeg \ ffmpeg \
libturbojpeg \ libturbojpeg \
libmariadb-dev-compat libmariadb-dev-compat \
libxml2-utils
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1084,7 +1093,8 @@ jobs:
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
@ -1152,6 +1162,12 @@ jobs:
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
path: coverage.xml path: coverage.xml
overwrite: true overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact - name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled() if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
@ -1200,7 +1216,8 @@ jobs:
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
ffmpeg \ ffmpeg \
libturbojpeg libturbojpeg \
libxml2-utils
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \ sudo apt-get -y install \
postgresql-server-dev-14 postgresql-server-dev-14
@ -1208,7 +1225,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1218,7 +1235,8 @@ jobs:
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
@ -1287,6 +1305,12 @@ jobs:
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
path: coverage.xml path: coverage.xml
overwrite: true overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact - name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled() if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
@ -1312,12 +1336,12 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.0 uses: codecov/codecov-action@v5.4.3
with: with:
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
@ -1354,12 +1378,13 @@ jobs:
bluez \ bluez \
ffmpeg \ ffmpeg \
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev \
libxml2-utils
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1369,7 +1394,8 @@ jobs:
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
@ -1432,6 +1458,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
overwrite: true overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact - name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled() if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v4.6.2
@ -1454,12 +1486,12 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.0 uses: codecov/codecov-action@v5.4.3
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -1479,7 +1511,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
pattern: test-results-* pattern: test-results-*
- name: Upload test results to Codecov - name: Upload test results to Codecov

View File

@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.12 uses: github/codeql-action/init@v3.28.18
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.12 uses: github/codeql-action/analyze@v3.28.18
with: with:
category: "/language:python" category: "/language:python"

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: env_file name: env_file
- name: Download build_constraints - name: Download build_constraints
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: build_constraints name: build_constraints
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: requirements_diff name: requirements_diff
@ -159,7 +159,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.02.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: env_file name: env_file
- name: Download build_constraints - name: Download build_constraints
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: build_constraints name: build_constraints
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: requirements_diff name: requirements_diff
- name: Download requirements_all_wheels - name: Download requirements_all_wheels
uses: actions/download-artifact@v4.2.1 uses: actions/download-artifact@v4.3.0
with: with:
name: requirements_all_wheels name: requirements_all_wheels
@ -219,7 +219,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.02.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -66,6 +66,7 @@ homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.* homeassistant.components.alert.*
homeassistant.components.alexa.* homeassistant.components.alexa.*
homeassistant.components.alpha_vantage.* homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_devices.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.* homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.* homeassistant.components.ambient_network.*
@ -119,6 +120,7 @@ homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.* homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.* homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.* homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.* homeassistant.components.braviatv.*
homeassistant.components.bring.* homeassistant.components.bring.*
homeassistant.components.brother.* homeassistant.components.brother.*
@ -269,6 +271,7 @@ homeassistant.components.image_processing.*
homeassistant.components.image_upload.* homeassistant.components.image_upload.*
homeassistant.components.imap.* homeassistant.components.imap.*
homeassistant.components.imgw_pib.* homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.* homeassistant.components.incomfort.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
@ -290,6 +293,7 @@ homeassistant.components.kaleidescape.*
homeassistant.components.knocki.* homeassistant.components.knocki.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.* homeassistant.components.kraken.*
homeassistant.components.kulersky.*
homeassistant.components.lacrosse.* homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.* homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.* homeassistant.components.lamarzocco.*
@ -330,6 +334,7 @@ homeassistant.components.media_player.*
homeassistant.components.media_source.* homeassistant.components.media_source.*
homeassistant.components.met_eireann.* homeassistant.components.met_eireann.*
homeassistant.components.metoffice.* homeassistant.components.metoffice.*
homeassistant.components.miele.*
homeassistant.components.mikrotik.* homeassistant.components.mikrotik.*
homeassistant.components.min_max.* homeassistant.components.min_max.*
homeassistant.components.minecraft_server.* homeassistant.components.minecraft_server.*
@ -361,8 +366,10 @@ homeassistant.components.no_ip.*
homeassistant.components.nordpool.* homeassistant.components.nordpool.*
homeassistant.components.notify.* homeassistant.components.notify.*
homeassistant.components.notion.* homeassistant.components.notion.*
homeassistant.components.ntfy.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.nut.* homeassistant.components.nut.*
homeassistant.components.ohme.*
homeassistant.components.onboarding.* homeassistant.components.onboarding.*
homeassistant.components.oncue.* homeassistant.components.oncue.*
homeassistant.components.onedrive.* homeassistant.components.onedrive.*
@ -380,8 +387,10 @@ homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.* homeassistant.components.p1_monitor.*
homeassistant.components.pandora.* homeassistant.components.pandora.*
homeassistant.components.panel_custom.* homeassistant.components.panel_custom.*
homeassistant.components.paperless_ngx.*
homeassistant.components.peblar.* homeassistant.components.peblar.*
homeassistant.components.peco.* homeassistant.components.peco.*
homeassistant.components.pegel_online.*
homeassistant.components.persistent_notification.* homeassistant.components.persistent_notification.*
homeassistant.components.person.* homeassistant.components.person.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
@ -428,7 +437,6 @@ homeassistant.components.roku.*
homeassistant.components.romy.* homeassistant.components.romy.*
homeassistant.components.rpi_power.* homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.* homeassistant.components.rss_feed_template.*
homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.russound_rio.* homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.* homeassistant.components.ruuvitag_ble.*
@ -458,6 +466,7 @@ homeassistant.components.slack.*
homeassistant.components.sleepiq.* homeassistant.components.sleepiq.*
homeassistant.components.smhi.* homeassistant.components.smhi.*
homeassistant.components.smlight.* homeassistant.components.smlight.*
homeassistant.components.smtp.*
homeassistant.components.snooz.* homeassistant.components.snooz.*
homeassistant.components.solarlog.* homeassistant.components.solarlog.*
homeassistant.components.sonarr.* homeassistant.components.sonarr.*

76
CODEOWNERS generated
View File

@ -46,8 +46,8 @@ build.json @home-assistant/supervisor
/tests/components/accuweather/ @bieniu /tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray /homeassistant/components/acmeda/ @atmurray
/tests/components/acmeda/ @atmurray /tests/components/acmeda/ @atmurray
/homeassistant/components/adax/ @danielhiversen /homeassistant/components/adax/ @danielhiversen @lazytarget
/tests/components/adax/ @danielhiversen /tests/components/adax/ @danielhiversen @lazytarget
/homeassistant/components/adguard/ @frenck /homeassistant/components/adguard/ @frenck
/tests/components/adguard/ @frenck /tests/components/adguard/ @frenck
/homeassistant/components/ads/ @mrpasztoradam /homeassistant/components/ads/ @mrpasztoradam
@ -89,6 +89,8 @@ build.json @home-assistant/supervisor
/tests/components/alert/ @home-assistant/core @frenck /tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amazon_devices/ @chemelli74
/tests/components/amazon_devices/ @chemelli74
/homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot /homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot
@ -171,6 +173,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/avea/ @pattyland /homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @danielsjf /homeassistant/components/awair/ @ahayworth @danielsjf
/tests/components/awair/ @ahayworth @danielsjf /tests/components/awair/ @ahayworth @danielsjf
/homeassistant/components/aws_s3/ @tomasbedrich
/tests/components/aws_s3/ @tomasbedrich
/homeassistant/components/axis/ @Kane610 /homeassistant/components/axis/ @Kane610
/tests/components/axis/ @Kane610 /tests/components/axis/ @Kane610
/homeassistant/components/azure_data_explorer/ @kaareseras /homeassistant/components/azure_data_explorer/ @kaareseras
@ -200,8 +204,8 @@ build.json @home-assistant/supervisor
/tests/components/blebox/ @bbx-a @swistakm /tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blink/ @fronzbot @mkmer /homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot @mkmer /tests/components/blink/ @fronzbot @mkmer
/homeassistant/components/blue_current/ @Floris272 @gleeuwen /homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/tests/components/blue_current/ @Floris272 @gleeuwen /tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/homeassistant/components/bluemaestro/ @bdraco /homeassistant/components/bluemaestro/ @bdraco
/tests/components/bluemaestro/ @bdraco /tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core /homeassistant/components/blueprint/ @home-assistant/core
@ -216,6 +220,8 @@ build.json @home-assistant/supervisor
/tests/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm /homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm /tests/components/bosch_shc/ @tschamm
/homeassistant/components/braviatv/ @bieniu @Drafteed /homeassistant/components/braviatv/ @bieniu @Drafteed
@ -299,6 +305,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/crownstone/ @Crownstone @RicArch97 /homeassistant/components/crownstone/ @Crownstone @RicArch97
/tests/components/crownstone/ @Crownstone @RicArch97 /tests/components/crownstone/ @Crownstone @RicArch97
/homeassistant/components/cups/ @fabaff /homeassistant/components/cups/ @fabaff
/tests/components/cups/ @fabaff
/homeassistant/components/daikin/ @fredrike /homeassistant/components/daikin/ @fredrike
/tests/components/daikin/ @fredrike /tests/components/daikin/ @fredrike
/homeassistant/components/date/ @home-assistant/core /homeassistant/components/date/ @home-assistant/core
@ -430,7 +437,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 /homeassistant/components/ephember/ @ttroy50 @roberty99
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame /homeassistant/components/epic_games_store/ @hacf-fr @Quentame
/tests/components/epic_games_store/ @hacf-fr @Quentame /tests/components/epic_games_store/ @hacf-fr @Quentame
/homeassistant/components/epion/ @lhgravendeel /homeassistant/components/epion/ @lhgravendeel
@ -451,8 +458,8 @@ build.json @home-assistant/supervisor
/tests/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb /homeassistant/components/evohome/ @zxdavb
/tests/components/evohome/ @zxdavb /tests/components/evohome/ @zxdavb
/homeassistant/components/ezviz/ @RenierM26 @baqs /homeassistant/components/ezviz/ @RenierM26
/tests/components/ezviz/ @RenierM26 @baqs /tests/components/ezviz/ @RenierM26
/homeassistant/components/faa_delays/ @ntilley905 /homeassistant/components/faa_delays/ @ntilley905
/tests/components/faa_delays/ @ntilley905 /tests/components/faa_delays/ @ntilley905
/homeassistant/components/fan/ @home-assistant/core /homeassistant/components/fan/ @home-assistant/core
@ -702,8 +709,12 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh /homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh /tests/components/imap/ @jbouwh
/homeassistant/components/imeon_inverter/ @Imeon-Energy
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu /homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu /tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/improv_ble/ @emontnemery /homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh /homeassistant/components/incomfort/ @jbouwh
@ -933,6 +944,8 @@ build.json @home-assistant/supervisor
/tests/components/metoffice/ @MrHarcombe @avee87 /tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/microbees/ @microBeesTech /homeassistant/components/microbees/ @microBeesTech
/tests/components/microbees/ @microBeesTech /tests/components/microbees/ @microBeesTech
/homeassistant/components/miele/ @astrandb
/tests/components/miele/ @astrandb
/homeassistant/components/mikrotik/ @engrbm87 /homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen /homeassistant/components/mill/ @danielhiversen
@ -1045,6 +1058,8 @@ build.json @home-assistant/supervisor
/tests/components/nsw_fuel_station/ @nickw444 /tests/components/nsw_fuel_station/ @nickw444
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte /homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
/tests/components/nsw_rural_fire_service_feed/ @exxamalte /tests/components/nsw_rural_fire_service_feed/ @exxamalte
/homeassistant/components/ntfy/ @tr4nt0r
/tests/components/ntfy/ @tr4nt0r
/homeassistant/components/nuheat/ @tstabrawa /homeassistant/components/nuheat/ @tstabrawa
/tests/components/nuheat/ @tstabrawa /tests/components/nuheat/ @tstabrawa
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree /homeassistant/components/nuki/ @pschmitt @pvizeli @pree
@ -1073,8 +1088,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/ombi/ @larssont /homeassistant/components/ombi/ @larssont
/homeassistant/components/onboarding/ @home-assistant/core /homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core /tests/components/onboarding/ @home-assistant/core
/homeassistant/components/oncue/ @bdraco @peterager
/tests/components/oncue/ @bdraco @peterager
/homeassistant/components/ondilo_ico/ @JeromeHXP /homeassistant/components/ondilo_ico/ @JeromeHXP
/tests/components/ondilo_ico/ @JeromeHXP /tests/components/ondilo_ico/ @JeromeHXP
/homeassistant/components/onedrive/ @zweckj /homeassistant/components/onedrive/ @zweckj
@ -1103,8 +1116,8 @@ build.json @home-assistant/supervisor
/tests/components/opentherm_gw/ @mvn23 /tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya /homeassistant/components/openuv/ @bachya
/tests/components/openuv/ @bachya /tests/components/openuv/ @bachya
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi /homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/tests/components/openweathermap/ @fabaff @freekode @nzapponi /tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/homeassistant/components/opnsense/ @mtreinish /homeassistant/components/opnsense/ @mtreinish
/tests/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish
/homeassistant/components/opower/ @tronikos /homeassistant/components/opower/ @tronikos
@ -1130,6 +1143,8 @@ build.json @home-assistant/supervisor
/tests/components/palazzetti/ @dotvav /tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend /homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend /tests/components/panel_custom/ @home-assistant/frontend
/homeassistant/components/paperless_ngx/ @fvgarrel
/tests/components/paperless_ngx/ @fvgarrel
/homeassistant/components/peblar/ @frenck /homeassistant/components/peblar/ @frenck
/tests/components/peblar/ @frenck /tests/components/peblar/ @frenck
/homeassistant/components/peco/ @IceBotYT /homeassistant/components/peco/ @IceBotYT
@ -1168,6 +1183,8 @@ build.json @home-assistant/supervisor
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson /tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/private_ble_device/ @Jc2k /homeassistant/components/private_ble_device/ @Jc2k
/tests/components/private_ble_device/ @Jc2k /tests/components/private_ble_device/ @Jc2k
/homeassistant/components/probe_plus/ @pantherale0
/tests/components/probe_plus/ @pantherale0
/homeassistant/components/profiler/ @bdraco /homeassistant/components/profiler/ @bdraco
/tests/components/profiler/ @bdraco /tests/components/profiler/ @bdraco
/homeassistant/components/progettihwsw/ @ardaseremet /homeassistant/components/progettihwsw/ @ardaseremet
@ -1183,6 +1200,8 @@ build.json @home-assistant/supervisor
/tests/components/prusalink/ @balloob /tests/components/prusalink/ @balloob
/homeassistant/components/ps4/ @ktnrg45 /homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45 /tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato
/tests/components/pterodactyl/ @elmurato
/homeassistant/components/pure_energie/ @klaasnicolaas /homeassistant/components/pure_energie/ @klaasnicolaas
/tests/components/pure_energie/ @klaasnicolaas /tests/components/pure_energie/ @klaasnicolaas
/homeassistant/components/purpleair/ @bachya /homeassistant/components/purpleair/ @bachya
@ -1212,6 +1231,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/qnap_qsw/ @Noltari /homeassistant/components/qnap_qsw/ @Noltari
/tests/components/qnap_qsw/ @Noltari /tests/components/qnap_qsw/ @Noltari
/homeassistant/components/quantum_gateway/ @cisasteelersfan /homeassistant/components/quantum_gateway/ @cisasteelersfan
/tests/components/quantum_gateway/ @cisasteelersfan
/homeassistant/components/qvr_pro/ @oblogic7 /homeassistant/components/qvr_pro/ @oblogic7
/homeassistant/components/qwikswitch/ @kellerza /homeassistant/components/qwikswitch/ @kellerza
/tests/components/qwikswitch/ @kellerza /tests/components/qwikswitch/ @kellerza
@ -1250,6 +1270,8 @@ build.json @home-assistant/supervisor
/tests/components/recovery_mode/ @home-assistant/core /tests/components/recovery_mode/ @home-assistant/core
/homeassistant/components/refoss/ @ashionky /homeassistant/components/refoss/ @ashionky
/tests/components/refoss/ @ashionky /tests/components/refoss/ @ashionky
/homeassistant/components/rehlko/ @bdraco @peterager
/tests/components/rehlko/ @bdraco @peterager
/homeassistant/components/remote/ @home-assistant/core /homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core /tests/components/remote/ @home-assistant/core
/homeassistant/components/remote_calendar/ @Thomas55555 /homeassistant/components/remote_calendar/ @Thomas55555
@ -1295,8 +1317,6 @@ build.json @home-assistant/supervisor
/tests/components/rpi_power/ @shenxn @swetoast /tests/components/rpi_power/ @shenxn @swetoast
/homeassistant/components/rss_feed_template/ @home-assistant/core /homeassistant/components/rss_feed_template/ @home-assistant/core
/tests/components/rss_feed_template/ @home-assistant/core /tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/russound_rio/ @noahhusby /homeassistant/components/russound_rio/ @noahhusby
@ -1383,7 +1403,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/siren/ @home-assistant/core @raman325 /homeassistant/components/siren/ @home-assistant/core @raman325
/tests/components/siren/ @home-assistant/core @raman325 /tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/sky_remote/ @dunnmj @saty9 /homeassistant/components/sky_remote/ @dunnmj @saty9
/tests/components/sky_remote/ @dunnmj @saty9 /tests/components/sky_remote/ @dunnmj @saty9
/homeassistant/components/skybell/ @tkdrob /homeassistant/components/skybell/ @tkdrob
@ -1401,6 +1420,8 @@ build.json @home-assistant/supervisor
/tests/components/sma/ @kellerza @rklomp @erwindouna /tests/components/sma/ @kellerza @rklomp @erwindouna
/homeassistant/components/smappee/ @bsmappee /homeassistant/components/smappee/ @bsmappee
/tests/components/smappee/ @bsmappee /tests/components/smappee/ @bsmappee
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
/tests/components/smarla/ @explicatis @rlint-explicatis
/homeassistant/components/smart_meter_texas/ @grahamwetzler /homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler /tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek /homeassistant/components/smartthings/ @joostlek
@ -1430,8 +1451,8 @@ build.json @home-assistant/supervisor
/tests/components/solarlog/ @Ernst79 @dontinelli /tests/components/solarlog/ @Ernst79 @dontinelli
/homeassistant/components/solax/ @squishykid @Darsstar /homeassistant/components/solax/ @squishykid @Darsstar
/tests/components/solax/ @squishykid @Darsstar /tests/components/solax/ @squishykid @Darsstar
/homeassistant/components/soma/ @ratsept @sebfortier2288 /homeassistant/components/soma/ @ratsept
/tests/components/soma/ @ratsept @sebfortier2288 /tests/components/soma/ @ratsept
/homeassistant/components/sonarr/ @ctalkington /homeassistant/components/sonarr/ @ctalkington
/tests/components/sonarr/ @ctalkington /tests/components/sonarr/ @ctalkington
/homeassistant/components/songpal/ @rytilahti @shenxn /homeassistant/components/songpal/ @rytilahti @shenxn
@ -1463,7 +1484,8 @@ build.json @home-assistant/supervisor
/tests/components/steam_online/ @tkdrob /tests/components/steam_online/ @tkdrob
/homeassistant/components/steamist/ @bdraco /homeassistant/components/steamist/ @bdraco
/tests/components/steamist/ @bdraco /tests/components/steamist/ @bdraco
/homeassistant/components/stiebel_eltron/ @fucm /homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
/homeassistant/components/stookwijzer/ @fwestenberg /homeassistant/components/stookwijzer/ @fwestenberg
/tests/components/stookwijzer/ @fwestenberg /tests/components/stookwijzer/ @fwestenberg
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter /homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
@ -1474,10 +1496,8 @@ build.json @home-assistant/supervisor
/tests/components/subaru/ @G-Two /tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii @jb101010-2 /homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii @jb101010-2 /tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig /homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @Swamp-Ig /tests/components/sun/ @home-assistant/core
/homeassistant/components/sunweg/ @rokam
/tests/components/sunweg/ @rokam
/homeassistant/components/supla/ @mwegrzynek /homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen /homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen /tests/components/surepetcare/ @benleb @danielhiversen
@ -1490,8 +1510,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili /homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur /homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur /tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza /homeassistant/components/switcher_kis/ @thecode @YogevBokobza
@ -1531,8 +1551,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj /tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike /homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core /homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core /tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks /homeassistant/components/tesla_wall_connector/ @einarhauks
@ -1668,8 +1688,8 @@ build.json @home-assistant/supervisor
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare /tests/components/vlc_telnet/ @rodripf @MartinHjelmare
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74 /homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
/tests/components/vodafone_station/ @paoloantinori @chemelli74 /tests/components/vodafone_station/ @paoloantinori @chemelli74
/homeassistant/components/voip/ @balloob @synesthesiam /homeassistant/components/voip/ @balloob @synesthesiam @jaminh
/tests/components/voip/ @balloob @synesthesiam /tests/components/voip/ @balloob @synesthesiam @jaminh
/homeassistant/components/volumio/ @OnFreund /homeassistant/components/volumio/ @OnFreund
/tests/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund
/homeassistant/components/volvooncall/ @molobrakos /homeassistant/components/volvooncall/ @molobrakos
@ -1786,6 +1806,8 @@ build.json @home-assistant/supervisor
/tests/components/zeversolar/ @kvanzuijlen /tests/components/zeversolar/ @kvanzuijlen
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES /homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES /tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/homeassistant/components/zimi/ @markhannon
/tests/components/zimi/ @markhannon
/homeassistant/components/zodiac/ @JulienTant /homeassistant/components/zodiac/ @JulienTant
/tests/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant
/homeassistant/components/zone/ @home-assistant/core /homeassistant/components/zone/ @home-assistant/core

2
Dockerfile generated
View File

@ -31,7 +31,7 @@ RUN \
&& go2rtc --version && go2rtc --version
# Install uv # Install uv
RUN pip3 install uv==0.6.8 RUN pip3 install uv==0.7.1
WORKDIR /usr/src WORKDIR /usr/src

View File

@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant image: ghcr.io/home-assistant/{arch}-homeassistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1 i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io
@ -19,4 +19,4 @@ labels:
org.opencontainers.image.authors: The Home Assistant Authors org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/ org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/ org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache License 2.0 org.opencontainers.image.licenses: Apache-2.0

View File

@ -53,6 +53,7 @@ from .components import (
logbook as logbook_pre_import, # noqa: F401 logbook as logbook_pre_import, # noqa: F401
lovelace as lovelace_pre_import, # noqa: F401 lovelace as lovelace_pre_import, # noqa: F401
onboarding as onboarding_pre_import, # noqa: F401 onboarding as onboarding_pre_import, # noqa: F401
person as person_pre_import, # noqa: F401
recorder as recorder_import, # noqa: F401 - not named pre_import since it has requirements recorder as recorder_import, # noqa: F401 - not named pre_import since it has requirements
repairs as repairs_pre_import, # noqa: F401 repairs as repairs_pre_import, # noqa: F401
search as search_pre_import, # noqa: F401 search as search_pre_import, # noqa: F401
@ -170,8 +171,6 @@ FRONTEND_INTEGRATIONS = {
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout. # Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
# The substage containing recorder should have no timeout, as it could cancel a database migration. # The substage containing recorder should have no timeout, as it could cancel a database migration.
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts. # Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
# The substages preceding it should also have no timeout, until we ensure that the recorder
# is not accidentally promoted as a dependency of any of the integrations in them.
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode. # If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
STAGE_0_INTEGRATIONS = ( STAGE_0_INTEGRATIONS = (
# Load logging and http deps as soon as possible # Load logging and http deps as soon as possible
@ -859,8 +858,14 @@ async def _async_set_up_integrations(
integrations, all_integrations = await _async_resolve_domains_and_preload( integrations, all_integrations = await _async_resolve_domains_and_preload(
hass, config hass, config
) )
all_domains = set(all_integrations) # Detect all cycles
domains = set(integrations) integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, all_integrations.values(), set(all_integrations)
)
)
all_domains = set(integrations_after_dependencies)
domains = set(integrations) & all_domains
_LOGGER.info( _LOGGER.info(
"Domains to be set up: %s | %s", "Domains to be set up: %s | %s",
@ -868,6 +873,8 @@ async def _async_set_up_integrations(
all_domains - domains, all_domains - domains,
) )
async_set_domains_to_be_loaded(hass, all_domains)
# Initialize recorder # Initialize recorder
if "recorder" in all_domains: if "recorder" in all_domains:
recorder.async_initialize_recorder(hass) recorder.async_initialize_recorder(hass)
@ -900,24 +907,12 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered = { stage_dep_domains_unfiltered = {
dep dep
for domain in stage_domains for domain in stage_domains
for dep in all_integrations[domain].all_dependencies for dep in integrations_after_dependencies[domain]
if dep not in stage_domains if dep not in stage_domains
} }
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
stage_all_domains = stage_domains | stage_dep_domains stage_all_domains = stage_domains | stage_dep_domains
stage_all_integrations = {
domain: all_integrations[domain] for domain in stage_all_domains
}
# Detect all cycles
stage_integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, stage_all_integrations.values(), stage_all_domains
)
)
stage_all_domains = set(stage_integrations_after_dependencies)
stage_domains &= stage_all_domains
stage_dep_domains &= stage_all_domains
_LOGGER.info( _LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s", "Setting up stage %s: %s | %s\nDependencies: %s | %s",
@ -928,13 +923,15 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered - stage_dep_domains, stage_dep_domains_unfiltered - stage_dep_domains,
) )
async_set_domains_to_be_loaded(hass, stage_all_domains)
if timeout is None: if timeout is None:
await _async_setup_multi_components(hass, stage_all_domains, config) await _async_setup_multi_components(hass, stage_all_domains, config)
continue continue
try: try:
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME): async with hass.timeout.async_timeout(
timeout,
cool_down=COOLDOWN_TIME,
cancel_message=f"Bootstrap stage {name} timeout",
):
await _async_setup_multi_components(hass, stage_all_domains, config) await _async_setup_multi_components(hass, stage_all_domains, config)
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
@ -946,7 +943,11 @@ async def _async_set_up_integrations(
# Wrap up startup # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _LOGGER.debug("Waiting for startup to wrap up")
try: try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): async with hass.timeout.async_timeout(
WRAP_UP_TIMEOUT,
cool_down=COOLDOWN_TIME,
cancel_message="Bootstrap startup wrap up timeout",
):
await hass.async_block_till_done() await hass.async_block_till_done()
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(

View File

@ -1,5 +1,13 @@
{ {
"domain": "amazon", "domain": "amazon",
"name": "Amazon", "name": "Amazon",
"integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"] "integrations": [
"alexa",
"amazon_devices",
"amazon_polly",
"aws",
"aws_s3",
"fire_tv",
"route53"
]
} }

View File

@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}

View File

@ -0,0 +1,5 @@
{
"domain": "eve",
"name": "Eve",
"iot_standards": ["matter"]
}

View File

@ -6,6 +6,7 @@
"google_assistant_sdk", "google_assistant_sdk",
"google_cloud", "google_cloud",
"google_drive", "google_drive",
"google_gemini",
"google_generative_ai_conversation", "google_generative_ai_conversation",
"google_mail", "google_mail",
"google_maps", "google_maps",

View File

@ -1,5 +1,6 @@
{ {
"domain": "motionblinds", "domain": "motionblinds",
"name": "Motionblinds", "name": "Motionblinds",
"integrations": ["motion_blinds", "motionblinds_ble"] "integrations": ["motion_blinds", "motionblinds_ble"],
"iot_standards": ["matter"]
} }

View File

@ -0,0 +1,6 @@
{
"domain": "nuki",
"name": "Nuki",
"integrations": ["nuki"],
"iot_standards": ["matter"]
}

View File

@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
2: "moderate", 2: "moderate",
3: "high", 3: "high",
4: "very_high", 4: "very_high",
5: "extreme",
} }
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40) UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)

View File

@ -72,10 +72,11 @@
"level": { "level": {
"name": "Level", "name": "Level",
"state": { "state": {
"high": "High", "extreme": "Extreme",
"low": "Low", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "Moderate", "moderate": "Moderate",
"very_high": "Very high" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -89,10 +90,11 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -123,10 +125,11 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -167,10 +170,11 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -181,10 +185,11 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@ -195,10 +200,11 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }

View File

@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries() entry.unique_id for entry in self._async_current_entries()
} }
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError): with suppress(TimeoutError):
async with timeout(5): async with timeout(5):
hubs: list[aiopulse.Hub] = [ hubs = [
hub hub
async for hub in aiopulse.Hub.discover() async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured if hub.id not in already_configured

View File

@ -2,25 +2,38 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import CONNECTION_TYPE, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
PLATFORMS = [Platform.CLIMATE] PLATFORMS = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
"""Set up Adax from a config entry.""" """Set up Adax from a config entry."""
if entry.data.get(CONNECTION_TYPE) == LOCAL:
local_coordinator = AdaxLocalCoordinator(hass, entry)
entry.runtime_data = local_coordinator
else:
cloud_coordinator = AdaxCloudCoordinator(hass, entry)
entry.runtime_data = cloud_coordinator
await entry.runtime_data.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_migrate_entry(
hass: HomeAssistant, config_entry: AdaxConfigEntry
) -> bool:
"""Migrate old entry.""" """Migrate old entry."""
# convert title and unique_id to string # convert title and unique_id to string
if config_entry.version == 1: if config_entry.version == 1:

View File

@ -12,57 +12,42 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_TOKEN,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
PRECISION_WHOLE, PRECISION_WHOLE,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL from . import AdaxConfigEntry
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxLocalCoordinator
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AdaxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the Adax thermostat with config flow.""" """Set up the Adax thermostat with config flow."""
if entry.data.get(CONNECTION_TYPE) == LOCAL: if entry.data.get(CONNECTION_TYPE) == LOCAL:
adax_data_handler = AdaxLocal( local_coordinator = cast(AdaxLocalCoordinator, entry.runtime_data)
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_TOKEN],
websession=async_get_clientsession(hass, verify_ssl=False),
)
async_add_entities( async_add_entities(
[LocalAdaxDevice(adax_data_handler, entry.data[CONF_UNIQUE_ID])], True [LocalAdaxDevice(local_coordinator, entry.data[CONF_UNIQUE_ID])],
)
else:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
async_add_entities(
AdaxDevice(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
) )
return
adax_data_handler = Adax(
entry.data[ACCOUNT_ID],
entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
)
async_add_entities(
(
AdaxDevice(room, adax_data_handler)
for room in await adax_data_handler.get_rooms()
),
True,
)
class AdaxDevice(ClimateEntity): class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
"""Representation of a heater.""" """Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
@ -76,20 +61,37 @@ class AdaxDevice(ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE _attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: def __init__(
self,
coordinator: AdaxCloudCoordinator,
device_id: str,
) -> None:
"""Initialize the heater.""" """Initialize the heater."""
self._device_id = heater_data["id"] super().__init__(coordinator)
self._adax_data_handler = adax_data_handler self._adax_data_handler: Adax = coordinator.adax_data_handler
self._device_id = device_id
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" self._attr_name = self.room["name"]
self._attr_unique_id = f"{self.room['homeId']}_{self._device_id}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, heater_data["id"])}, identifiers={(DOMAIN, device_id)},
# Instead of setting the device name to the entity name, adax # Instead of setting the device name to the entity name, adax
# should be updated to set has_entity_name = True, and set the entity # should be updated to set has_entity_name = True, and set the entity
# name to None # name to None
name=cast(str | None, self.name), name=cast(str | None, self.name),
manufacturer="Adax", manufacturer="Adax",
) )
self._apply_data(self.room)
@property
def available(self) -> bool:
"""Whether the entity is available or not."""
return super().available and self._device_id in self.coordinator.data
@property
def room(self) -> dict[str, Any]:
"""Gets the data for this particular device."""
return self.coordinator.data[self._device_id]
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode.""" """Set hvac mode."""
@ -104,7 +106,9 @@ class AdaxDevice(ClimateEntity):
) )
else: else:
return return
await self._adax_data_handler.update()
# Request data refresh from source to verify that update was successful
await self.coordinator.async_request_refresh()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
@ -114,28 +118,31 @@ class AdaxDevice(ClimateEntity):
self._device_id, temperature, True self._device_id, temperature, True
) )
async def async_update(self) -> None: @callback
"""Get the latest data.""" def _handle_coordinator_update(self) -> None:
for room in await self._adax_data_handler.get_rooms(): """Handle updated data from the coordinator."""
if room["id"] != self._device_id: if room := self.room:
continue self._apply_data(room)
self._attr_name = room["name"] super()._handle_coordinator_update()
self._attr_current_temperature = room.get("temperature")
self._attr_target_temperature = room.get("targetTemperature") def _apply_data(self, room: dict[str, Any]) -> None:
if room["heatingEnabled"]: """Update the appropriate attributues based on received data."""
self._attr_hvac_mode = HVACMode.HEAT self._attr_current_temperature = room.get("temperature")
self._attr_icon = "mdi:radiator" self._attr_target_temperature = room.get("targetTemperature")
else: if room["heatingEnabled"]:
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator-off" self._attr_icon = "mdi:radiator"
return else:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
class LocalAdaxDevice(ClimateEntity): class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
"""Representation of a heater.""" """Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_hvac_mode = HVACMode.HEAT _attr_hvac_mode = HVACMode.OFF
_attr_icon = "mdi:radiator-off"
_attr_max_temp = 35 _attr_max_temp = 35
_attr_min_temp = 5 _attr_min_temp = 5
_attr_supported_features = ( _attr_supported_features = (
@ -146,9 +153,10 @@ class LocalAdaxDevice(ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE _attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None: def __init__(self, coordinator: AdaxLocalCoordinator, unique_id: str) -> None:
"""Initialize the heater.""" """Initialize the heater."""
self._adax_data_handler = adax_data_handler super().__init__(coordinator)
self._adax_data_handler: AdaxLocal = coordinator.adax_data_handler
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)}, identifiers={(DOMAIN, unique_id)},
@ -169,17 +177,20 @@ class LocalAdaxDevice(ClimateEntity):
return return
await self._adax_data_handler.set_target_temperature(temperature) await self._adax_data_handler.set_target_temperature(temperature)
async def async_update(self) -> None: @callback
"""Get the latest data.""" def _handle_coordinator_update(self) -> None:
data = await self._adax_data_handler.get_status() """Handle updated data from the coordinator."""
self._attr_current_temperature = data["current_temperature"] if data := self.coordinator.data:
self._attr_available = self._attr_current_temperature is not None self._attr_current_temperature = data["current_temperature"]
if (target_temp := data["target_temperature"]) == 0: self._attr_available = self._attr_current_temperature is not None
self._attr_hvac_mode = HVACMode.OFF if (target_temp := data["target_temperature"]) == 0:
self._attr_icon = "mdi:radiator-off" self._attr_hvac_mode = HVACMode.OFF
if target_temp == 0: self._attr_icon = "mdi:radiator-off"
self._attr_target_temperature = self._attr_min_temp if target_temp == 0:
else: self._attr_target_temperature = self._attr_min_temp
self._attr_hvac_mode = HVACMode.HEAT else:
self._attr_icon = "mdi:radiator" self._attr_hvac_mode = HVACMode.HEAT
self._attr_target_temperature = target_temp self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp
super()._handle_coordinator_update()

View File

@ -1,5 +1,6 @@
"""Constants for the Adax integration.""" """Constants for the Adax integration."""
import datetime
from typing import Final from typing import Final
ACCOUNT_ID: Final = "account_id" ACCOUNT_ID: Final = "account_id"
@ -9,3 +10,5 @@ DOMAIN: Final = "adax"
LOCAL = "Local" LOCAL = "Local"
WIFI_SSID = "wifi_ssid" WIFI_SSID = "wifi_ssid"
WIFI_PSWD = "wifi_pswd" WIFI_PSWD = "wifi_pswd"
SCAN_INTERVAL = datetime.timedelta(seconds=60)

View File

@ -0,0 +1,71 @@
"""DataUpdateCoordinator for the Adax component."""
import logging
from typing import Any, cast
from adax import Adax
from adax_local import Adax as AdaxLocal
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ACCOUNT_ID, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
type AdaxConfigEntry = ConfigEntry[AdaxCloudCoordinator | AdaxLocalCoordinator]
class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
"""Coordinator for updating data to and from Adax (cloud)."""
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
"""Initialize the Adax coordinator used for Cloud mode."""
super().__init__(
hass,
config_entry=entry,
logger=_LOGGER,
name="AdaxCloud",
update_interval=SCAN_INTERVAL,
)
self.adax_data_handler = Adax(
entry.data[ACCOUNT_ID],
entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
)
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Adax."""
rooms = await self.adax_data_handler.get_rooms() or []
return {r["id"]: r for r in rooms}
class AdaxLocalCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
"""Coordinator for updating data to and from Adax (local)."""
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
"""Initialize the Adax coordinator used for Local mode."""
super().__init__(
hass,
config_entry=entry,
logger=_LOGGER,
name="AdaxLocal",
update_interval=SCAN_INTERVAL,
)
self.adax_data_handler = AdaxLocal(
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_TOKEN],
websession=async_get_clientsession(hass, verify_ssl=False),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from the Adax."""
if result := await self.adax_data_handler.get_status():
return cast(dict[str, Any], result)
raise UpdateFailed("Got invalid status from device")

View File

@ -1,7 +1,7 @@
{ {
"domain": "adax", "domain": "adax",
"name": "Adax", "name": "Adax",
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen", "@lazytarget"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adax", "documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
from .entity import AdvantageAirEntity, AdvantageAirThingEntity from .entity import AdvantageAirEntity, AdvantageAirThingEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
self._id: str = light["id"] self._id: str = light["id"]
self._attr_unique_id += f"-{self._id}" self._attr_unique_id += f"-{self._id}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, identifiers={(DOMAIN, self._attr_unique_id)},
via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]), via_device=(DOMAIN, self.coordinator.data["system"]["rid"]),
manufacturer="Advantage Air", manufacturer="Advantage Air",
model=light.get("moduleType"), model=light.get("moduleType"),
name=light["name"], name=light["name"],

View File

@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry from . import AdvantageAirDataConfigEntry
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import DOMAIN
from .entity import AdvantageAirEntity from .entity import AdvantageAirEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@ -32,9 +32,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
"""Initialize the Advantage Air App.""" """Initialize the Advantage Air App."""
super().__init__(instance) super().__init__(instance)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={ identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"])
},
manufacturer="Advantage Air", manufacturer="Advantage Air",
model=self.coordinator.data["system"]["sysType"], model=self.coordinator.data["system"]["sysType"],
name=self.coordinator.data["system"]["name"], name=self.coordinator.data["system"]["name"],

View File

@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL from .const import DOMAIN, SERVER_URL
ATTRIBUTION = "ispyconnect.com" ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com" DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
@ -46,7 +46,7 @@ async def async_setup_entry(
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, config_entry_id=config_entry.entry_id,
identifiers={(AGENT_DOMAIN, agent_client.unique)}, identifiers={(DOMAIN, agent_client.unique)},
manufacturer="iSpyConnect", manufacturer="iSpyConnect",
name=f"Agent {agent_client.name}", name=f"Agent {agent_client.name}",
model="Agent DVR", model="Agent DVR",

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AgentDVRConfigEntry from . import AgentDVRConfigEntry
from .const import DOMAIN as AGENT_DOMAIN from .const import DOMAIN
CONF_HOME_MODE_NAME = "home" CONF_HOME_MODE_NAME = "home"
CONF_AWAY_MODE_NAME = "away" CONF_AWAY_MODE_NAME = "away"
@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
self._client = client self._client = client
self._attr_unique_id = f"{client.unique}_CP" self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, client.unique)}, identifiers={(DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}", name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent", manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME, model=CONST_ALARM_CONTROL_PANEL_NAME,

View File

@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
) )
from . import AgentDVRConfigEntry from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS) SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001 still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
) )
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Agent", manufacturer="Agent",
model="Camera", model="Camera",
name=f"{device.client.name} {device.name}", name=f"{device.client.name} {device.name}",

View File

@ -6,6 +6,7 @@ from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -29,6 +30,7 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
model_id=measures.model, model_id=measures.model,
serial_number=coordinator.serial_number, serial_number=coordinator.serial_number,
sw_version=measures.firmware_version, sw_version=measures.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
) )

View File

@ -68,8 +68,8 @@
"led_bar_mode": { "led_bar_mode": {
"name": "LED bar mode", "name": "LED bar mode",
"state": { "state": {
"off": "Off", "off": "[%key:common::state::off%]",
"co2": "Carbon dioxide", "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "Particulate matter" "pm": "Particulate matter"
} }
}, },
@ -143,8 +143,8 @@
"led_bar_mode": { "led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": { "state": {
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]", "off": "[%key:common::state::off%]",
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]", "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
} }
}, },

View File

@ -8,7 +8,7 @@ from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError from pyairnow.errors import AirNowError, InvalidJsonError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
distance=self.distance, distance=self.distance,
) )
except (AirNowError, ClientConnectorError) as error: except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
if not obs: if not obs:

View File

@ -5,23 +5,22 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from airthings import Airthings, AirthingsDevice, AirthingsError from airthings import Airthings
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET, DOMAIN from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6) SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]] type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass), async_get_clientsession(hass),
) )
async def _update_method() -> dict[str, AirthingsDevice]: coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator entry.runtime_data = coordinator

View File

@ -0,0 +1,36 @@
"""The Airthings integration."""
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,
)
self.airthings = airthings
async def _update_method(self) -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await self.airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

View File

@ -3,6 +3,19 @@
"name": "Airthings", "name": "Airthings",
"codeowners": ["@danielhiversen", "@LaStrada"], "codeowners": ["@danielhiversen", "@LaStrada"],
"config_flow": true, "config_flow": true,
"dhcp": [
{
"hostname": "airthings-view"
},
{
"hostname": "airthings-hub",
"macaddress": "D0141190*"
},
{
"hostname": "airthings-hub",
"macaddress": "70B3D52A0*"
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings", "documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["airthings"], "loggers": ["airthings"],

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
EntityCategory, EntityCategory,
@ -26,8 +27,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType from . import AirthingsConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = { SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription( "radonShortTermAvg": SensorEntityDescription(
@ -78,6 +80,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="light", translation_key="light",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
"virusRisk": SensorEntityDescription( "virusRisk": SensorEntityDescription(
key="virusRisk", key="virusRisk",
translation_key="virus_risk", translation_key="virus_risk",
@ -133,7 +141,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor( class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
): ):
"""Representation of a Airthings Sensor device.""" """Representation of a Airthings Sensor device."""
@ -142,7 +150,7 @@ class AirthingsHeaterEnergySensor(
def __init__( def __init__(
self, self,
coordinator: AirthingsDataCoordinatorType, coordinator: AirthingsDataUpdateCoordinator,
airthings_device: AirthingsDevice, airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription, entity_description: SensorEntityDescription,
) -> None: ) -> None:

View File

@ -102,7 +102,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
@ -160,7 +161,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device) self._discovered_devices[address] = Discovery(name, discovery_info, device)

View File

@ -142,7 +142,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode] return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode]
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number) airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number)
modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes] modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes]
@ -226,12 +226,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
return super()._handle_coordinator_update() return super()._handle_coordinator_update()
@property @property
def min_temp(self): def min_temp(self) -> float:
"""Return Minimum Temperature for AC of this group.""" """Return Minimum Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint
@property @property
def max_temp(self): def max_temp(self) -> float:
"""Return Max Temperature for AC of this group.""" """Return Max Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint

View File

@ -32,7 +32,8 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
client = Airtouch5SimpleClient(user_input[CONF_HOST]) client = Airtouch5SimpleClient(user_input[CONF_HOST])
try: try:
await client.test_connection() await client.test_connection()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unexpected exception")
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
else: else:
await self.async_set_unique_id(user_input[CONF_HOST]) await self.async_set_unique_id(user_input[CONF_HOST])

View File

@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"geography_by_coords": { "geography_by_coords": {
"title": "Configure a Geography", "title": "Configure a geography",
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.", "description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
@ -16,8 +16,8 @@
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City", "city": "City",
"country": "Country", "state": "State",
"state": "State" "country": "[%key:common::config_flow::data::country%]"
} }
}, },
"reauth_confirm": { "reauth_confirm": {
@ -56,12 +56,12 @@
"sensor": { "sensor": {
"pollutant_label": { "pollutant_label": {
"state": { "state": {
"co": "Carbon Monoxide", "co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"n2": "Nitrogen Dioxide", "n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"o3": "Ozone", "o3": "[%key:component::sensor::entity_component::ozone::name%]",
"p1": "PM10", "p1": "[%key:component::sensor::entity_component::pm10::name%]",
"p2": "PM2.5", "p2": "[%key:component::sensor::entity_component::pm25::name%]",
"s2": "Sulfur Dioxide" "s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
} }
}, },
"pollutant_level": { "pollutant_level": {

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.9"] "requirements": ["aioairzone==1.0.0"]
} }

View File

@ -9,6 +9,8 @@ from aioairzone.const import (
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_TEMP, AZD_TEMP,
AZD_TEMP_UNIT, AZD_TEMP_UNIT,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_SIGNAL,
AZD_WEBSERVER, AZD_WEBSERVER,
AZD_WIFI_RSSI, AZD_WIFI_RSSI,
AZD_ZONES, AZD_ZONES,
@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription(
device_class=SensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_THERMOSTAT_SIGNAL,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="thermostat_signal",
),
) )

View File

@ -76,6 +76,9 @@
"sensor": { "sensor": {
"rssi": { "rssi": {
"name": "RSSI" "name": "RSSI"
},
"thermostat_signal": {
"name": "Signal strength"
} }
} }
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.10"] "requirements": ["aioairzone-cloud==0.6.12"]
} }

View File

@ -32,9 +32,9 @@
"air_quality": { "air_quality": {
"name": "Air Quality mode", "name": "Air Quality mode",
"state": { "state": {
"off": "Off", "off": "[%key:common::state::off%]",
"on": "On", "on": "[%key:common::state::on%]",
"auto": "Auto" "auto": "[%key:common::state::auto%]"
} }
}, },
"modes": { "modes": {

View File

@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Choose AlarmDecoder Protocol", "title": "Choose AlarmDecoder protocol",
"data": { "data": {
"protocol": "Protocol" "protocol": "Protocol"
} }
@ -12,8 +12,8 @@
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]", "port": "[%key:common::config_flow::data::port%]",
"device_baudrate": "Device Baud Rate", "device_baudrate": "Device baud rate",
"device_path": "Device Path" "device_path": "Device path"
}, },
"data_description": { "data_description": {
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.", "host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
@ -44,36 +44,36 @@
"arm_settings": { "arm_settings": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"data": { "data": {
"auto_bypass": "Auto Bypass on Arm", "auto_bypass": "Auto-bypass on arm",
"code_arm_required": "Code Required for Arming", "code_arm_required": "Code required for arming",
"alt_night_mode": "Alternative Night Mode" "alt_night_mode": "Alternative night mode"
} }
}, },
"zone_select": { "zone_select": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter the zone number you'd like to to add, edit, or remove.", "description": "Enter the zone number you'd like to to add, edit, or remove.",
"data": { "data": {
"zone_number": "Zone Number" "zone_number": "Zone number"
} }
}, },
"zone_details": { "zone_details": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]", "title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.",
"data": { "data": {
"zone_name": "Zone Name", "zone_name": "Zone name",
"zone_type": "Zone Type", "zone_type": "Zone type",
"zone_rfid": "RF Serial", "zone_rfid": "RF serial",
"zone_loop": "RF Loop", "zone_loop": "RF loop",
"zone_relayaddr": "Relay Address", "zone_relayaddr": "Relay address",
"zone_relaychan": "Relay Channel" "zone_relaychan": "Relay channel"
} }
} }
}, },
"error": { "error": {
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", "relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.",
"int": "The field below must be an integer.", "int": "The field below must be an integer.",
"loop_rfid": "RF Loop cannot be used without RF Serial.", "loop_rfid": "'RF loop' cannot be used without 'RF serial'.",
"loop_range": "RF Loop must be an integer between 1 and 4." "loop_range": "'RF loop' must be an integer between 1 and 4."
} }
}, },
"services": { "services": {

View File

@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
# Fan preset_mode # Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None) mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None): if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, ()):
return f"{fan.ATTR_PRESET_MODE}.{mode}" return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Humidifier mode # Humidifier mode

View File

@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
yield Alexa(self.entity) yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN) @ENTITY_ADAPTERS.register(media_player.DOMAIN)
class MediaPlayerCapabilities(AlexaEntity): class MediaPlayerCapabilities(AlexaEntity):
"""Class to represent MediaPlayer capabilities.""" """Class to represent MediaPlayer capabilities."""
@ -757,9 +757,7 @@ class MediaPlayerCapabilities(AlexaEntity):
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE: if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
inputs = AlexaInputController.get_valid_inputs( inputs = AlexaInputController.get_valid_inputs(
self.entity.attributes.get( self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
media_player.const.ATTR_INPUT_SOURCE_LIST, []
)
) )
if len(inputs) > 0: if len(inputs) > 0:
yield AlexaInputController(self.entity) yield AlexaInputController(self.entity)
@ -776,8 +774,7 @@ class MediaPlayerCapabilities(AlexaEntity):
and domain != "denonavr" and domain != "denonavr"
): ):
inputs = AlexaEqualizerController.get_valid_inputs( inputs = AlexaEqualizerController.get_valid_inputs(
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
or []
) )
if len(inputs) > 0: if len(inputs) > 0:
yield AlexaEqualizerController(self.entity) yield AlexaEqualizerController(self.entity)

View File

@ -566,7 +566,7 @@ async def async_api_set_volume(
data: dict[str, Any] = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
await hass.services.async_call( await hass.services.async_call(
@ -589,7 +589,7 @@ async def async_api_select_input(
# Attempt to map the ALL UPPERCASE payload name to a source. # Attempt to map the ALL UPPERCASE payload name to a source.
# Strips trailing 1 to match single input devices. # Strips trailing 1 to match single input devices.
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or [] source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
for source in source_list: for source in source_list:
formatted_source = ( formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "") source.lower().replace("-", "").replace("_", "").replace(" ", "")
@ -611,7 +611,7 @@ async def async_api_select_input(
data: dict[str, Any] = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_INPUT_SOURCE: media_input, media_player.ATTR_INPUT_SOURCE: media_input,
} }
await hass.services.async_call( await hass.services.async_call(
@ -636,7 +636,7 @@ async def async_api_adjust_volume(
volume_delta = int(directive.payload["volume"]) volume_delta = int(directive.payload["volume"])
entity = directive.entity entity = directive.entity
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL] current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
# read current state # read current state
try: try:
@ -648,7 +648,7 @@ async def async_api_adjust_volume(
data: dict[str, Any] = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
await hass.services.async_call( await hass.services.async_call(
@ -709,7 +709,7 @@ async def async_api_set_mute(
entity = directive.entity entity = directive.entity
data: dict[str, Any] = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
} }
await hass.services.async_call( await hass.services.async_call(
@ -1708,15 +1708,13 @@ async def async_api_changechannel(
data: dict[str, Any] = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_CONTENT_ID: channel, media_player.ATTR_MEDIA_CONTENT_ID: channel,
media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
media_player.const.MEDIA_TYPE_CHANNEL
),
} }
await hass.services.async_call( await hass.services.async_call(
entity.domain, entity.domain,
media_player.const.SERVICE_PLAY_MEDIA, media_player.SERVICE_PLAY_MEDIA,
data, data,
blocking=False, blocking=False,
context=context, context=context,
@ -1825,13 +1823,13 @@ async def async_api_set_eq_mode(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a SetMode request for EqualizerController.""" """Process a SetMode request for EqualizerController."""
mode = directive.payload["mode"] mode: str = directive.payload["mode"]
entity = directive.entity entity = directive.entity
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
if sound_mode_list and mode.lower() in sound_mode_list: if sound_mode_list and mode.lower() in sound_mode_list:
data[media_player.const.ATTR_SOUND_MODE] = mode.lower() data[media_player.ATTR_SOUND_MODE] = mode.lower()
else: else:
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}" msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)

View File

@ -3,10 +3,10 @@
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from asyncio import timeout
from collections.abc import Mapping
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4 from uuid import uuid4
@ -260,10 +260,10 @@ async def async_enable_proactive_mode(
def extra_significant_check( def extra_significant_check(
hass: HomeAssistant, hass: HomeAssistant,
old_state: str, old_state: str,
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any], old_attrs: Mapping[Any, Any],
old_extra_arg: Any, old_extra_arg: Any,
new_state: str, new_state: str,
new_attrs: dict[str, Any] | MappingProxyType[Any, Any], new_attrs: Mapping[Any, Any],
new_extra_arg: Any, new_extra_arg: Any,
) -> bool: ) -> bool:
"""Check if the serialized data has changed.""" """Check if the serialized data has changed."""

View File

@ -0,0 +1,32 @@
"""Amazon Devices integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.NOTIFY,
Platform.SWITCH,
]
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Amazon Devices platform."""
coordinator = AmazonDevicesCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.api.close()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,71 @@
"""Support for binary sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Final
from aioamazondevices.api import AmazonDevice
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Amazon Devices binary sensor entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on_fn=lambda _device: _device.online,
),
AmazonBinarySensorEntityDescription(
key="bluetooth",
translation_key="bluetooth",
is_on_fn=lambda _device: _device.bluetooth_state,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Amazon Devices binary sensors based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
)
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
"""Binary sensor device."""
entity_description: AmazonBinarySensorEntityDescription
@property
def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self.entity_description.is_on_fn(self.device)

View File

@ -0,0 +1,63 @@
"""Config flow for Amazon Devices integration."""
from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonEchoApi
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Amazon Devices."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input:
client = AmazonEchoApi(
user_input[CONF_COUNTRY],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
try:
data = await client.login_mode_interactive(user_input[CONF_CODE])
except CannotConnect:
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
user_input.pop(CONF_CODE)
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input | {CONF_LOGIN_DATA: data},
)
finally:
await client.close()
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_COUNTRY, default=self.hass.config.country
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
}
),
)

View File

@ -0,0 +1,8 @@
"""Amazon Devices constants."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "amazon_devices"
CONF_LOGIN_DATA = "login_data"

View File

@ -0,0 +1,58 @@
"""Support for Amazon Devices."""
from datetime import timedelta
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CONF_LOGIN_DATA
SCAN_INTERVAL = 30
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Base coordinator for Amazon Devices."""
config_entry: AmazonConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AmazonConfigEntry,
) -> None:
"""Initialize the scanner."""
super().__init__(
hass,
_LOGGER,
name=entry.title,
config_entry=entry,
update_interval=timedelta(seconds=SCAN_INTERVAL),
)
self.api = AmazonEchoApi(
entry.data[CONF_COUNTRY],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
)
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
try:
await self.api.login_mode_stored_data()
return await self.api.get_devices_data()
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
except CannotAuthenticate as err:
raise ConfigEntryError("Could not authenticate") from err

View File

@ -0,0 +1,57 @@
"""Defines a base Amazon Devices entity."""
from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SPEAKER_GROUP_MODEL
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AmazonDevicesCoordinator
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines a base Amazon Devices entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AmazonDevicesCoordinator,
serial_num: str,
description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model_details = coordinator.api.get_model_details(self.device)
model = model_details["model"] if model_details else None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model_id=self.device.device_type,
manufacturer="Amazon",
hw_version=model_details["hw_version"] if model_details else None,
sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
),
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"
@property
def device(self) -> AmazonDevice:
"""Return the device."""
return self.coordinator.data[self._serial_num]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self._serial_num in self.coordinator.data
and self.device.online
)

View File

@ -0,0 +1,12 @@
{
"entity": {
"binary_sensor": {
"bluetooth": {
"default": "mdi:bluetooth",
"state": {
"off": "mdi:bluetooth-off"
}
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"domain": "amazon_devices",
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "08A6BC*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "440049*" },
{ "macaddress": "443D54*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "74D637*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "901195*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" },
{ "macaddress": "F02F9E*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==2.1.1"]
}

View File

@ -0,0 +1,74 @@
"""Support for notification entity."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Final
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonNotifyEntityDescription(NotifyEntityDescription):
"""Amazon Devices notify entity description."""
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
subkey: str
NOTIFY: Final = (
AmazonNotifyEntityDescription(
key="speak",
translation_key="speak",
subkey="AUDIO_PLAYER",
method=lambda api, device, message: api.call_alexa_speak(device, message),
),
AmazonNotifyEntityDescription(
key="announce",
translation_key="announce",
subkey="AUDIO_PLAYER",
method=lambda api, device, message: api.call_alexa_announcement(
device, message
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Amazon Devices notification entity based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
)
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
"""Binary sensor notify platform."""
entity_description: AmazonNotifyEntityDescription
async def async_send_message(
self, message: str, title: str | None = None, **kwargs: Any
) -> None:
"""Send a message."""
await self.entity_description.method(self.coordinator.api, self.device, message)

View File

@ -0,0 +1,74 @@
rules:
# Bronze
action-setup:
status: exempt
comment: no actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: no actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: entities do not explicitly subscribe to events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage:
status: todo
comment: all tests missing
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Network information not relevant
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: automate the cleanup process
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: done

View File

@ -0,0 +1,60 @@
{
"common": {
"data_country": "Country code",
"data_code": "One-time password (OTP code)",
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
},
"config": {
"flow_title": "{username}",
"step": {
"user": {
"data": {
"country": "[%key:component::amazon_devices::common::data_country%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
},
"data_description": {
"country": "[%key:component::amazon_devices::common::data_description_country%]",
"username": "[%key:component::amazon_devices::common::data_description_username%]",
"password": "[%key:component::amazon_devices::common::data_description_password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"binary_sensor": {
"bluetooth": {
"name": "Bluetooth"
}
},
"notify": {
"speak": {
"name": "Speak"
},
"announce": {
"name": "Announce"
}
},
"switch": {
"do_not_disturb": {
"name": "Do not disturb"
}
}
}
}

View File

@ -0,0 +1,84 @@
"""Support for switches."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Final
from aioamazondevices.api import AmazonDevice
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonSwitchEntityDescription(SwitchEntityDescription):
"""Amazon Devices switch entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
subkey: str
method: str
SWITCHES: Final = (
AmazonSwitchEntityDescription(
key="do_not_disturb",
subkey="AUDIO_PLAYER",
translation_key="do_not_disturb",
is_on_fn=lambda _device: _device.do_not_disturb,
method="set_do_not_disturb",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Amazon Devices switches based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in coordinator.data
if switch_desc.subkey in coordinator.data[serial_num].capabilities
)
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
"""Switch device."""
entity_description: AmazonSwitchEntityDescription
async def _switch_set_state(self, state: bool) -> None:
"""Set desired switch state."""
method = getattr(self.coordinator.api, self.entity_description.method)
if TYPE_CHECKING:
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self._switch_set_state(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self._switch_set_state(False)
@property
def is_on(self) -> bool:
"""Return True if switch is on."""
return self.entity_description.is_on_fn(self.device)

View File

@ -6,5 +6,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"], "loggers": ["boto3", "botocore", "s3transfer"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["boto3==1.34.131"] "requirements": ["boto3==1.37.1"]
} }

View File

@ -240,6 +240,7 @@ SENSOR_DESCRIPTIONS = (
suggested_display_precision=0, suggested_display_precision=0,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTMPH, key=TYPE_WINDGUSTMPH,

View File

@ -609,6 +609,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="wind_direction", translation_key="wind_direction",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M, key=TYPE_WINDDIR_AVG10M,

View File

@ -3,12 +3,12 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"tracked_addons": "Addons", "tracked_addons": "Add-ons",
"tracked_integrations": "Integrations", "tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations" "tracked_custom_integrations": "Custom integrations"
}, },
"data_description": { "data_description": {
"tracked_addons": "Select the addons you want to track", "tracked_addons": "Select the add-ons you want to track",
"tracked_integrations": "Select the integrations you want to track", "tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track" "tracked_custom_integrations": "Select the custom integrations you want to track"
} }

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["pydroid-ipcam==2.0.0"] "requirements": ["pydroid-ipcam==3.0.0"]
} }

View File

@ -73,7 +73,7 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_key_command(key_code, direction) self._api.send_key_command(key_code, direction)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc
def _send_launch_app_command(self, app_link: str) -> None: def _send_launch_app_command(self, app_link: str) -> None:
@ -85,5 +85,5 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_launch_app_command(app_link) self._api.send_launch_app_command(app_link)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc

View File

@ -7,6 +7,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["androidtvremote2"], "loggers": ["androidtvremote2"],
"requirements": ["androidtvremote2==0.2.0"], "requirements": ["androidtvremote2==0.2.2"],
"zeroconf": ["_androidtvremote2._tcp.local."] "zeroconf": ["_androidtvremote2._tcp.local."]
} }

View File

@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AndroidTVRemoteConfigEntry from . import AndroidTVRemoteConfigEntry
from .const import CONF_APP_ICON, CONF_APP_NAME from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
from .entity import AndroidTVRemoteBaseEntity from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -233,5 +233,5 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
await asyncio.sleep(delay_secs) await asyncio.sleep(delay_secs)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc

View File

@ -51,8 +51,17 @@
"app_id": "Application ID", "app_id": "Application ID",
"app_icon": "Application icon", "app_icon": "Application icon",
"app_delete": "Check to delete this application" "app_delete": "Check to delete this application"
},
"data_description": {
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename"
} }
} }
} }
},
"exceptions": {
"connection_closed": {
"message": "Connection to the Android TV device is closed"
}
} }
} }

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
import logging
from anova_wifi import AnovaApi, InvalidLogin from anova_wifi import AnovaApi, InvalidLogin
import voluptuous as vol import voluptuous as vol
@ -11,8 +13,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
class AnovaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova.""" """Sets up a config flow for Anova."""
VERSION = 1 VERSION = 1
@ -35,7 +39,8 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
await api.authenticate() await api.authenticate()
except InvalidLogin: except InvalidLogin:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
return self.async_create_entry( return self.async_create_entry(

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from functools import partial from functools import partial
import logging import logging
from types import MappingProxyType from types import MappingProxyType
@ -52,7 +53,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
RECOMMENDED_OPTIONS = { RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True, CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT, CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
} }
@ -134,9 +135,8 @@ class AnthropicOptionsFlow(OptionsFlow):
if user_input is not None: if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended: if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none": if not user_input.get(CONF_LLM_HASS_API):
user_input.pop(CONF_LLM_HASS_API) user_input.pop(CONF_LLM_HASS_API, None)
if user_input.get( if user_input.get(
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS): ) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
@ -151,12 +151,16 @@ class AnthropicOptionsFlow(OptionsFlow):
options = { options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED], CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT], CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API], CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
} }
suggested_values = options.copy() suggested_values = options.copy()
if not suggested_values.get(CONF_PROMPT): if not suggested_values.get(CONF_PROMPT):
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
if (
suggested_llm_apis := suggested_values.get(CONF_LLM_HASS_API)
) and isinstance(suggested_llm_apis, str):
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
schema = self.add_suggested_values_to_schema( schema = self.add_suggested_values_to_schema(
vol.Schema(anthropic_config_option_schema(self.hass, options)), vol.Schema(anthropic_config_option_schema(self.hass, options)),
@ -172,28 +176,22 @@ class AnthropicOptionsFlow(OptionsFlow):
def anthropic_config_option_schema( def anthropic_config_option_schema(
hass: HomeAssistant, hass: HomeAssistant,
options: dict[str, Any] | MappingProxyType[str, Any], options: Mapping[str, Any],
) -> dict: ) -> dict:
"""Return a schema for Anthropic completion options.""" """Return a schema for Anthropic completion options."""
hass_apis: list[SelectOptionDict] = [ hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label="No control",
value="none",
)
]
hass_apis.extend(
SelectOptionDict( SelectOptionDict(
label=api.name, label=api.name,
value=api.id, value=api.id,
) )
for api in llm.async_get_apis(hass) for api in llm.async_get_apis(hass)
) ]
schema = { schema = {
vol.Optional(CONF_PROMPT): TemplateSelector(), vol.Optional(CONF_PROMPT): TemplateSelector(),
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector( vol.Optional(
SelectSelectorConfig(options=hass_apis) CONF_LLM_HASS_API,
), ): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
vol.Required( vol.Required(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False) CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
): bool, ): bool,

View File

@ -17,4 +17,11 @@ CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0 RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024 MIN_THINKING_BUDGET = 1024
THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-3-7-sonnet-latest"] THINKING_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"claude-opus-4-20250514",
"claude-opus-4-0",
"claude-sonnet-4-20250514",
"claude-sonnet-4-0",
]

View File

@ -9,11 +9,13 @@ from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN from anthropic._types import NOT_GIVEN
from anthropic.types import ( from anthropic.types import (
InputJSONDelta, InputJSONDelta,
MessageDeltaUsage,
MessageParam, MessageParam,
MessageStreamEvent, MessageStreamEvent,
RawContentBlockDeltaEvent, RawContentBlockDeltaEvent,
RawContentBlockStartEvent, RawContentBlockStartEvent,
RawContentBlockStopEvent, RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent, RawMessageStartEvent,
RawMessageStopEvent, RawMessageStopEvent,
RedactedThinkingBlock, RedactedThinkingBlock,
@ -31,6 +33,7 @@ from anthropic.types import (
ToolResultBlockParam, ToolResultBlockParam,
ToolUseBlock, ToolUseBlock,
ToolUseBlockParam, ToolUseBlockParam,
Usage,
) )
from voluptuous_openapi import convert from voluptuous_openapi import convert
@ -162,7 +165,8 @@ def _convert_content(
return messages return messages
async def _transform_stream( async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent], result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam], messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]: ) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
@ -207,6 +211,7 @@ async def _transform_stream(
| None | None
) = None ) = None
current_tool_args: str current_tool_args: str
input_usage: Usage | None = None
async for response in result: async for response in result:
LOGGER.debug("Received response: %s", response) LOGGER.debug("Received response: %s", response)
@ -215,6 +220,7 @@ async def _transform_stream(
if response.message.role != "assistant": if response.message.role != "assistant":
raise ValueError("Unexpected message role") raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[]) current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent): elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock): if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam( current_block = ToolUseBlockParam(
@ -265,32 +271,56 @@ async def _transform_stream(
if current_block is None: if current_block is None:
raise ValueError("Unexpected stop event without a current block") raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use": if current_block["type"] == "tool_use":
tool_block = cast(ToolUseBlockParam, current_block) # tool block
tool_args = json.loads(current_tool_args) tool_args = json.loads(current_tool_args) if current_tool_args else {}
tool_block["input"] = tool_args current_block["input"] = tool_args
yield { yield {
"tool_calls": [ "tool_calls": [
llm.ToolInput( llm.ToolInput(
id=tool_block["id"], id=current_block["id"],
tool_name=tool_block["name"], tool_name=current_block["name"],
tool_args=tool_args, tool_args=tool_args,
) )
] ]
} }
elif current_block["type"] == "thinking": elif current_block["type"] == "thinking":
thinking_block = cast(ThinkingBlockParam, current_block) # thinking block
LOGGER.debug("Thinking: %s", thinking_block["thinking"]) LOGGER.debug("Thinking: %s", current_block["thinking"])
if current_message is None: if current_message is None:
raise ValueError("Unexpected stop event without a current message") raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr] current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent): elif isinstance(response, RawMessageStopEvent):
if current_message is not None: if current_message is not None:
messages.append(current_message) messages.append(current_message)
current_message = None current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicConversationEntity( class AnthropicConversationEntity(
conversation.ConversationEntity, conversation.AbstractConversationAgent conversation.ConversationEntity, conversation.AbstractConversationAgent
): ):
@ -298,6 +328,7 @@ class AnthropicConversationEntity(
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None
_attr_supports_streaming = True
def __init__(self, entry: AnthropicConfigEntry) -> None: def __init__(self, entry: AnthropicConfigEntry) -> None:
"""Initialize the agent.""" """Initialize the agent."""
@ -393,7 +424,8 @@ class AnthropicConversationEntity(
[ [
content content
async for content in chat_log.async_add_delta_content_stream( async for content in chat_log.async_add_delta_content_stream(
user_input.agent_id, _transform_stream(stream, messages) user_input.agent_id,
_transform_stream(chat_log, stream, messages),
) )
if not isinstance(content, conversation.AssistantContent) if not isinstance(content, conversation.AssistantContent)
] ]

View File

@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic", "documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["anthropic==0.47.2"] "requirements": ["anthropic==0.52.0"]
} }

View File

@ -53,10 +53,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
"""Initialize the APCUPSd binary device.""" """Initialize the APCUPSd binary device."""
super().__init__(coordinator, context=description.key.upper()) super().__init__(coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available.
if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
@property @property

View File

@ -46,11 +46,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="user", data_schema=_SCHEMA) return self.async_show_form(step_id="user", data_schema=_SCHEMA)
host, port = user_input[CONF_HOST], user_input[CONF_PORT] host, port = user_input[CONF_HOST], user_input[CONF_PORT]
# Abort if an entry with same host and port is present.
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
# Test the connection to the host and get the current status for serial number.
try: try:
async with asyncio.timeout(CONNECTION_TIMEOUT): async with asyncio.timeout(CONNECTION_TIMEOUT):
data = APCUPSdData(await aioapcaccess.request_status(host, port)) data = APCUPSdData(await aioapcaccess.request_status(host, port))
@ -67,3 +63,30 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
title = data.name or data.model or data.serial_no or "APC UPS" title = data.name or data.model or data.serial_no or "APC UPS"
return self.async_create_entry(title=title, data=user_input) return self.async_create_entry(title=title, data=user_input)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
if user_input is None:
return self.async_show_form(step_id="reconfigure", data_schema=_SCHEMA)
host, port = user_input[CONF_HOST], user_input[CONF_PORT]
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
try:
async with asyncio.timeout(CONNECTION_TIMEOUT):
data = APCUPSdData(await aioapcaccess.request_status(host, port))
except (OSError, asyncio.IncompleteReadError, TimeoutError):
errors = {"base": "cannot_connect"}
return self.async_show_form(
step_id="reconfigure", data_schema=_SCHEMA, errors=errors
)
await self.async_set_unique_id(data.serial_no)
self._abort_if_unique_id_mismatch(reason="wrong_apcupsd_daemon")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates=user_input,
)

View File

@ -85,11 +85,16 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
self._host = host self._host = host
self._port = port self._port = port
@property
def unique_device_id(self) -> str:
"""Return a unique ID of the device, which is the serial number (if available) or the config entry ID."""
return self.data.serial_no or self.config_entry.entry_id
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the DeviceInfo of this APC UPS, if serial number is available.""" """Return the DeviceInfo of this APC UPS, if serial number is available."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.data.serial_no or self.config_entry.entry_id)}, identifiers={(DOMAIN, self.unique_device_id)},
model=self.data.model, model=self.data.model,
manufacturer="APC", manufacturer="APC",
name=self.data.name or "APC UPS", name=self.data.name or "APC UPS",
@ -108,4 +113,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
data = await aioapcaccess.request_status(self._host, self._port) data = await aioapcaccess.request_status(self._host, self._port)
return APCUPSdData(data) return APCUPSdData(data)
except (OSError, asyncio.IncompleteReadError) as error: except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from error

View File

@ -458,11 +458,8 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator=coordinator, context=description.key.upper()) super().__init__(coordinator=coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available.
if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
# Initial update of attributes. # Initial update of attributes.

View File

@ -1,7 +1,9 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"wrong_apcupsd_daemon": "The reconfigured APC UPS Daemon is not the same as the one already configured.",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
@ -93,7 +95,7 @@
"name": "Internal temperature" "name": "Internal temperature"
}, },
"last_self_test": { "last_self_test": {
"name": "Last self test" "name": "Last self-test"
}, },
"last_transfer": { "last_transfer": {
"name": "Last transfer" "name": "Last transfer"
@ -177,7 +179,7 @@
"name": "Restore requirement" "name": "Restore requirement"
}, },
"self_test_result": { "self_test_result": {
"name": "Self test result" "name": "Self-test result"
}, },
"sensitivity": { "sensitivity": {
"name": "Sensitivity" "name": "Sensitivity"
@ -195,7 +197,7 @@
"name": "Status" "name": "Status"
}, },
"self_test_interval": { "self_test_interval": {
"name": "Self test interval" "name": "Self-test interval"
}, },
"time_left": { "time_left": {
"name": "Time left" "name": "Time left"
@ -219,5 +221,10 @@
"name": "Transfer to battery" "name": "Transfer to battery"
} }
} }
},
"exceptions": {
"cannot_connect": {
"message": "Cannot connect to APC UPS Daemon."
}
} }
} }

View File

@ -20,6 +20,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_IGNORE, SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_ZEROCONF, SOURCE_ZEROCONF,
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IDENTIFIERS: list(combined_identifiers), CONF_IDENTIFIERS: list(combined_identifiers),
}, },
) )
if entry.source != SOURCE_IGNORE: # Don't reload ignored entries or in the middle of reauth,
# e.g. if the user is entering a new PIN
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
self.hass.config_entries.async_schedule_reload(entry.entry_id) self.hass.config_entries.async_schedule_reload(entry.entry_id)
if not allow_exist: if not allow_exist:
raise DeviceAlreadyConfigured raise DeviceAlreadyConfigured

View File

@ -120,6 +120,7 @@ class AppleTvMediaPlayer(
"""Initialize the Apple TV media player.""" """Initialize the Apple TV media player."""
super().__init__(name, identifier, manager) super().__init__(name, identifier, manager)
self._playing: Playing | None = None self._playing: Playing | None = None
self._playing_last_updated: datetime | None = None
self._app_list: dict[str, str] = {} self._app_list: dict[str, str] = {}
@callback @callback
@ -209,6 +210,7 @@ class AppleTvMediaPlayer(
This is a callback function from pyatv.interface.PushListener. This is a callback function from pyatv.interface.PushListener.
""" """
self._playing = playstatus self._playing = playstatus
self._playing_last_updated = dt_util.utcnow()
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
@ -316,7 +318,7 @@ class AppleTvMediaPlayer(
def media_position_updated_at(self) -> datetime | None: def media_position_updated_at(self) -> datetime | None:
"""Last valid time of media position.""" """Last valid time of media position."""
if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
return dt_util.utcnow() return self._playing_last_updated
return None return None
async def async_play_media( async def async_play_media(

View File

@ -62,6 +62,8 @@ async def async_setup_entry(
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT, target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
min_humidity=10, min_humidity=10,
max_humidity=50, max_humidity=50,
auto_status_key=Attribute.HUMIDIFICATION_AVAILABLE,
auto_status_value=1,
default_humidity=30, default_humidity=30,
set_humidity_fn=coordinator.client.set_humidification_setpoint, set_humidity_fn=coordinator.client.set_humidification_setpoint,
) )
@ -77,6 +79,8 @@ async def async_setup_entry(
action_map=DEHUMIDIFIER_ACTION_MAP, action_map=DEHUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT, target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
auto_status_key=None,
auto_status_value=None,
min_humidity=40, min_humidity=40,
max_humidity=90, max_humidity=90,
default_humidity=60, default_humidity=60,
@ -100,6 +104,8 @@ class AprilaireHumidifierDescription(HumidifierEntityDescription):
target_humidity_key: str target_humidity_key: str
min_humidity: int min_humidity: int
max_humidity: int max_humidity: int
auto_status_key: str | None
auto_status_value: int | None
default_humidity: int default_humidity: int
set_humidity_fn: Callable[[int], Awaitable] set_humidity_fn: Callable[[int], Awaitable]
@ -163,14 +169,31 @@ class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
def min_humidity(self) -> float: def min_humidity(self) -> float:
"""Return the minimum humidity.""" """Return the minimum humidity."""
if self.is_auto_humidity_mode():
return 1
return self.entity_description.min_humidity return self.entity_description.min_humidity
@property @property
def max_humidity(self) -> float: def max_humidity(self) -> float:
"""Return the maximum humidity.""" """Return the maximum humidity."""
if self.is_auto_humidity_mode():
return 7
return self.entity_description.max_humidity return self.entity_description.max_humidity
def is_auto_humidity_mode(self) -> bool:
"""Return whether the humidifier is in auto mode."""
if self.entity_description.auto_status_key is None:
return False
return (
self.coordinator.data.get(self.entity_description.auto_status_key)
== self.entity_description.auto_status_value
)
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set the humidity.""" """Set the humidity."""

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyaprilaire"], "loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.8.1"] "requirements": ["pyaprilaire==0.9.1"]
} }

View File

@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
config_entry: ApSystemsConfigEntry config_entry: ApSystemsConfigEntry
device_version: str device_version: str
battery_system: bool
def __init__( def __init__(
self, self,
@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
self.api.max_power = device_info.maxPower self.api.max_power = device_info.maxPower
self.api.min_power = device_info.minPower self.api.min_power = device_info.minPower
self.device_version = device_info.devVer self.device_version = device_info.devVer
self.battery_system = device_info.isBatterySystem
async def _async_update_data(self) -> ApSystemsSensorData: async def _async_update_data(self) -> ApSystemsSensorData:
try: try:

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems", "documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["apsystems-ez1==2.4.0"] "loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.6.0"]
} }

View File

@ -21,7 +21,7 @@
"entity": { "entity": {
"binary_sensor": { "binary_sensor": {
"off_grid_status": { "off_grid_status": {
"name": "Off grid status" "name": "Off-grid status"
}, },
"dc_1_short_circuit_error_status": { "dc_1_short_circuit_error_status": {
"name": "DC 1 short circuit error status" "name": "DC 1 short circuit error status"

View File

@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
super().__init__(data) super().__init__(data)
self._api = data.coordinator.api self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_inverter_status" self._attr_unique_id = f"{data.device_id}_inverter_status"
if data.coordinator.battery_system:
self._attr_available = False
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update switch status and availability.""" """Update switch status and availability."""

View File

@ -60,7 +60,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except AuthenticationFailed: except AuthenticationFailed:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:

View File

@ -1,6 +1,9 @@
{ {
"entity": { "entity": {
"sensor": { "sensor": {
"last_update": {
"default": "mdi:update"
},
"salt_left_side_percentage": { "salt_left_side_percentage": {
"default": "mdi:basket-fill" "default": "mdi:basket-fill"
}, },

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from aioaquacell import Softener from aioaquacell import Softener
@ -28,7 +29,7 @@ PARALLEL_UPDATES = 1
class SoftenerSensorEntityDescription(SensorEntityDescription): class SoftenerSensorEntityDescription(SensorEntityDescription):
"""Describes Softener sensor entity.""" """Describes Softener sensor entity."""
value_fn: Callable[[Softener], StateType] value_fn: Callable[[Softener], StateType | datetime]
SENSORS: tuple[SoftenerSensorEntityDescription, ...] = ( SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
@ -77,6 +78,12 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
"low", "low",
], ],
), ),
SoftenerSensorEntityDescription(
key="last_update",
translation_key="last_update",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda softener: softener.lastUpdate,
),
) )
@ -111,6 +118,6 @@ class SoftenerSensor(AquacellEntity, SensorEntity):
self.entity_description = description self.entity_description = description
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType | datetime:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.value_fn(self.softener) return self.entity_description.value_fn(self.softener)

View File

@ -21,6 +21,9 @@
}, },
"entity": { "entity": {
"sensor": { "sensor": {
"last_update": {
"name": "Last update"
},
"salt_left_side_percentage": { "salt_left_side_percentage": {
"name": "Salt left side percentage" "name": "Salt left side percentage"
}, },
@ -36,9 +39,9 @@
"wi_fi_strength": { "wi_fi_strength": {
"name": "Wi-Fi strength", "name": "Wi-Fi strength",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
} }

View File

@ -26,7 +26,7 @@
"sensor": { "sensor": {
"threshold": { "threshold": {
"state": { "state": {
"error": "Error", "error": "[%key:common::state::error%]",
"green": "Green", "green": "Green",
"yellow": "Yellow", "yellow": "Yellow",
"red": "Red" "red": "Red"

View File

@ -6,7 +6,11 @@ import logging
from typing import Any from typing import Any
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -98,6 +102,7 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
DEGREE, DEGREE,
"mdi:compass", "mdi:compass",
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
] ]
return None return None
@ -178,6 +183,7 @@ class ArwnSensor(SensorEntity):
units: str, units: str,
icon: str | None = None, icon: str | None = None,
device_class: SensorDeviceClass | None = None, device_class: SensorDeviceClass | None = None,
state_class: SensorStateClass | None = None,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.entity_id = _slug(name) self.entity_id = _slug(name)
@ -188,6 +194,7 @@ class ArwnSensor(SensorEntity):
self._attr_native_unit_of_measurement = units self._attr_native_unit_of_measurement = units
self._attr_icon = icon self._attr_icon = icon
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_state_class = state_class
def set_event(self, event: dict[str, Any]) -> None: def set_event(self, event: dict[str, Any]) -> None:
"""Update the sensor with the most recent event.""" """Update the sensor with the most recent event."""

View File

@ -20,9 +20,6 @@ import hass_nabucasa
import voluptuous as vol import voluptuous as vol
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
from homeassistant.components.tts import (
generate_media_source_id as tts_generate_media_source_id,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
from homeassistant.core import Context, HomeAssistant, callback from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -92,6 +89,8 @@ KEY_ASSIST_PIPELINE: HassKey[PipelineData] = HassKey(DOMAIN)
KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey( KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey(
"pipeline_conversation_data" "pipeline_conversation_data"
) )
# Number of response parts to handle before streaming the response
STREAM_RESPONSE_CHARS = 60
def validate_language(data: dict[str, Any]) -> Any: def validate_language(data: dict[str, Any]) -> Any:
@ -125,7 +124,7 @@ SAVE_DELAY = 10
@callback @callback
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool: def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
"""Filter out intents that are not local fallback.""" """Filter out intents that are not local fallback."""
return result.intent.name in (intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND) return result.intent.name in (intent.INTENT_GET_STATE)
@callback @callback
@ -555,7 +554,7 @@ class PipelineRun:
event_callback: PipelineEventCallback event_callback: PipelineEventCallback
language: str = None # type: ignore[assignment] language: str = None # type: ignore[assignment]
runner_data: Any | None = None runner_data: Any | None = None
intent_agent: str | None = None intent_agent: conversation.AgentInfo | None = None
tts_audio_output: str | dict[str, Any] | None = None tts_audio_output: str | dict[str, Any] | None = None
wake_word_settings: WakeWordSettings | None = None wake_word_settings: WakeWordSettings | None = None
audio_settings: AudioSettings = field(default_factory=AudioSettings) audio_settings: AudioSettings = field(default_factory=AudioSettings)
@ -591,6 +590,9 @@ class PipelineRun:
_intent_agent_only = False _intent_agent_only = False
"""If request should only be handled by agent, ignoring sentence triggers and local processing.""" """If request should only be handled by agent, ignoring sentence triggers and local processing."""
_streamed_response_text = False
"""If the conversation agent streamed response text to TTS result."""
def __post_init__(self) -> None: def __post_init__(self) -> None:
"""Set language for pipeline.""" """Set language for pipeline."""
self.language = self.pipeline.language or self.hass.config.language self.language = self.pipeline.language or self.hass.config.language
@ -652,6 +654,11 @@ class PipelineRun:
"token": self.tts_stream.token, "token": self.tts_stream.token,
"url": self.tts_stream.url, "url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type, "mime_type": self.tts_stream.content_type,
"stream_response": (
self.tts_stream.supports_streaming_input
and self.intent_agent
and self.intent_agent.supports_streaming
),
} }
self.process_event(PipelineEvent(PipelineEventType.RUN_START, data)) self.process_event(PipelineEvent(PipelineEventType.RUN_START, data))
@ -899,12 +906,12 @@ class PipelineRun:
) -> str: ) -> str:
"""Run speech-to-text portion of pipeline. Returns the spoken text.""" """Run speech-to-text portion of pipeline. Returns the spoken text."""
# Create a background task to prepare the conversation agent # Create a background task to prepare the conversation agent
if self.end_stage >= PipelineStage.INTENT: if self.end_stage >= PipelineStage.INTENT and self.intent_agent:
self.hass.async_create_background_task( self.hass.async_create_background_task(
conversation.async_prepare_agent( conversation.async_prepare_agent(
self.hass, self.intent_agent, self.language self.hass, self.intent_agent.id, self.language
), ),
f"prepare conversation agent {self.intent_agent}", f"prepare conversation agent {self.intent_agent.id}",
) )
if isinstance(self.stt_provider, stt.Provider): if isinstance(self.stt_provider, stt.Provider):
@ -1045,7 +1052,7 @@ class PipelineRun:
message=f"Intent recognition engine {engine} is not found", message=f"Intent recognition engine {engine} is not found",
) )
self.intent_agent = agent_info.id self.intent_agent = agent_info
async def recognize_intent( async def recognize_intent(
self, self,
@ -1078,7 +1085,7 @@ class PipelineRun:
PipelineEvent( PipelineEvent(
PipelineEventType.INTENT_START, PipelineEventType.INTENT_START,
{ {
"engine": self.intent_agent, "engine": self.intent_agent.id,
"language": input_language, "language": input_language,
"intent_input": intent_input, "intent_input": intent_input,
"conversation_id": conversation_id, "conversation_id": conversation_id,
@ -1095,11 +1102,11 @@ class PipelineRun:
conversation_id=conversation_id, conversation_id=conversation_id,
device_id=device_id, device_id=device_id,
language=input_language, language=input_language,
agent_id=self.intent_agent, agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt, extra_system_prompt=conversation_extra_system_prompt,
) )
agent_id = self.intent_agent agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
intent_response: intent.IntentResponse | None = None intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only: if not processed_locally and not self._intent_agent_only:
@ -1121,7 +1128,7 @@ class PipelineRun:
# If the LLM has API access, we filter out some sentences that are # If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation. # interfering with LLM operation.
if ( if (
intent_agent_state := self.hass.states.get(self.intent_agent) intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get( ) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0 ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL: ) & conversation.ConversationEntityFeature.CONTROL:
@ -1143,6 +1150,13 @@ class PipelineRun:
agent_id = conversation.HOME_ASSISTANT_AGENT agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True processed_locally = True
if self.tts_stream and self.tts_stream.supports_streaming_input:
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
else:
tts_input_stream = None
chat_log_role = None
delta_character_count = 0
@callback @callback
def chat_log_delta_listener( def chat_log_delta_listener(
chat_log: conversation.ChatLog, delta: dict chat_log: conversation.ChatLog, delta: dict
@ -1156,6 +1170,61 @@ class PipelineRun:
}, },
) )
) )
if tts_input_stream is None:
return
nonlocal chat_log_role
if role := delta.get("role"):
chat_log_role = role
# We are only interested in assistant deltas
if chat_log_role != "assistant":
return
if content := delta.get("content"):
tts_input_stream.put_nowait(content)
if self._streamed_response_text:
return
nonlocal delta_character_count
# Streamed responses are not cached. That's why we only start streaming text after
# we have received enough characters that indicates it will be a long response
# or if we have received text, and then a tool call.
# Tool call after we already received text
start_streaming = delta_character_count > 0 and delta.get("tool_calls")
# Count characters in the content and test if we exceed streaming threshold
if not start_streaming and content:
delta_character_count += len(content)
start_streaming = delta_character_count > STREAM_RESPONSE_CHARS
if not start_streaming:
return
self._streamed_response_text = True
async def tts_input_stream_generator() -> AsyncGenerator[str]:
"""Yield TTS input stream."""
while (tts_input := await tts_input_stream.get()) is not None:
yield tts_input
# Concatenate all existing queue items
parts = []
while not tts_input_stream.empty():
parts.append(tts_input_stream.get_nowait())
tts_input_stream.put_nowait(
"".join(
# At this point parts is only strings, None indicates end of queue
cast(list[str], parts)
)
)
assert self.tts_stream is not None
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
with ( with (
chat_session.async_get_chat_session( chat_session.async_get_chat_session(
@ -1199,6 +1268,8 @@ class PipelineRun:
speech = conversation_result.response.speech.get("plain", {}).get( speech = conversation_result.response.speech.get("plain", {}).get(
"speech", "" "speech", ""
) )
if tts_input_stream and self._streamed_response_text:
tts_input_stream.put_nowait(None)
except Exception as src_error: except Exception as src_error:
_LOGGER.exception("Unexpected error during intent recognition") _LOGGER.exception("Unexpected error during intent recognition")
@ -1276,26 +1347,11 @@ class PipelineRun:
) )
) )
try: if not self._streamed_response_text:
# Synthesize audio and get URL self.tts_stream.async_set_message(tts_input)
tts_media_id = tts_generate_media_source_id(
self.hass,
tts_input,
engine=self.tts_stream.engine,
language=self.tts_stream.language,
options=self.tts_stream.options,
)
except Exception as src_error:
_LOGGER.exception("Unexpected error during text-to-speech")
raise TextToSpeechError(
code="tts-failed",
message="Unexpected error during text-to-speech",
) from src_error
self.tts_stream.async_set_message(tts_input)
tts_output = { tts_output = {
"media_id": tts_media_id, "media_id": self.tts_stream.media_source_id,
"token": self.tts_stream.token, "token": self.tts_stream.token,
"url": self.tts_stream.url, "url": self.tts_stream.url,
"mime_type": self.tts_stream.content_type, "mime_type": self.tts_stream.content_type,

View File

@ -1,9 +1,11 @@
"""Base class for assist satellite entities.""" """Base class for assist satellite entities."""
import logging import logging
from pathlib import Path
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -15,6 +17,8 @@ from .const import (
CONNECTION_TEST_DATA, CONNECTION_TEST_DATA,
DATA_COMPONENT, DATA_COMPONENT,
DOMAIN, DOMAIN,
PREANNOUNCE_FILENAME,
PREANNOUNCE_URL,
AssistSatelliteEntityFeature, AssistSatelliteEntityFeature,
) )
from .entity import ( from .entity import (
@ -56,6 +60,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("message"): str, vol.Optional("message"): str,
vol.Optional("media_id"): str, vol.Optional("media_id"): str,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce_media_id"): str,
} }
), ),
cv.has_at_least_one_key("message", "media_id"), cv.has_at_least_one_key("message", "media_id"),
@ -70,6 +76,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("start_message"): str, vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str, vol.Optional("start_media_id"): str,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce_media_id"): str,
vol.Optional("extra_system_prompt"): str, vol.Optional("extra_system_prompt"): str,
} }
), ),
@ -82,6 +90,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_websocket_api(hass) async_register_websocket_api(hass)
hass.http.register_view(ConnectionTestView()) hass.http.register_view(ConnectionTestView())
# Default preannounce sound
await hass.http.async_register_static_paths(
[
StaticPathConfig(
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
)
]
)
return True return True

View File

@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
f"{DOMAIN}_connection_tests" f"{DOMAIN}_connection_tests"
) )
PREANNOUNCE_FILENAME = "preannounce.mp3"
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
class AssistSatelliteEntityFeature(IntFlag): class AssistSatelliteEntityFeature(IntFlag):
"""Supported features of Assist satellite entity.""" """Supported features of Assist satellite entity."""

Some files were not shown because too many files have changed in this diff Show More