mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Merge branch 'dev' into mill
This commit is contained in:
commit
93b0051ade
16
.github/workflows/builder.yml
vendored
16
.github/workflows/builder.yml
vendored
@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -330,14 +330,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -462,7 +462,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@ -502,7 +502,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
84
.github/workflows/ci.yaml
vendored
84
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 11
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
@ -255,7 +255,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -271,7 +271,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@ -301,7 +301,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -310,7 +310,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -341,7 +341,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -350,7 +350,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -381,7 +381,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -390,7 +390,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -497,7 +497,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -505,7 +505,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@ -552,7 +552,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@ -593,7 +593,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -626,7 +626,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -683,7 +683,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -695,7 +695,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
@ -726,7 +726,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -773,7 +773,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -825,7 +825,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -833,7 +833,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@ -895,7 +895,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -907,7 +907,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@ -955,7 +955,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -968,7 +968,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@ -1007,21 +1007,21 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
@ -1080,7 +1080,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1138,7 +1138,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@ -1146,7 +1146,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@ -1154,7 +1154,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@ -1214,7 +1214,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1273,7 +1273,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@ -1281,7 +1281,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@ -1289,7 +1289,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: test-results-postgres-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@ -1312,7 +1312,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@ -1365,7 +1365,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1420,21 +1420,21 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
@ -1454,7 +1454,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@ -1479,7 +1479,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.11
|
||||
uses: github/codeql-action/init@v3.28.12
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.11
|
||||
uses: github/codeql-action/analyze@v3.28.12
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
22
.github/workflows/wheels.yml
vendored
22
.github/workflows/wheels.yml
vendored
@ -91,7 +91,7 @@ jobs:
|
||||
) > build_constraints.txt
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@ -99,14 +99,14 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload build_constraints
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: build_constraints
|
||||
path: ./build_constraints.txt
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@ -118,7 +118,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@ -138,17 +138,17 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@ -187,22 +187,22 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
@ -412,6 +412,7 @@ homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.remote_calendar.*
|
||||
homeassistant.components.renault.*
|
||||
homeassistant.components.reolink.*
|
||||
homeassistant.components.repairs.*
|
||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@ -570,8 +570,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_cloud/ @lufton @tronikos
|
||||
/homeassistant/components/google_drive/ @tronikos
|
||||
/tests/components/google_drive/ @tronikos
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos @ivanlh
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos @ivanlh
|
||||
/homeassistant/components/google_mail/ @tkdrob
|
||||
/tests/components/google_mail/ @tkdrob
|
||||
/homeassistant/components/google_photos/ @allenporter
|
||||
@ -1252,6 +1252,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/remote/ @home-assistant/core
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
/homeassistant/components/remote_calendar/ @Thomas55555
|
||||
/tests/components/remote_calendar/ @Thomas55555
|
||||
/homeassistant/components/renault/ @epenet
|
||||
/tests/components/renault/ @epenet
|
||||
/homeassistant/components/renson/ @jimmyd-be
|
||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.1
|
||||
RUN pip3 install uv==0.6.8
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
@ -75,7 +75,11 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
||||
async with timeout(10):
|
||||
result = await self.accuweather.async_get_current_conditions()
|
||||
except EXCEPTIONS as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="current_conditions_update_error",
|
||||
translation_placeholders={"error": repr(error)},
|
||||
) from error
|
||||
|
||||
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
||||
|
||||
@ -121,7 +125,11 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||
language=self.hass.config.language
|
||||
)
|
||||
except EXCEPTIONS as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="forecast_update_error",
|
||||
translation_placeholders={"error": repr(error)},
|
||||
) from error
|
||||
|
||||
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
||||
|
||||
|
@ -229,6 +229,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"current_conditions_update_error": {
|
||||
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
|
||||
},
|
||||
"forecast_update_error": {
|
||||
"message": "An error occurred while retrieving weather forecast data from the AccuWeather API: {error}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach AccuWeather server",
|
||||
|
@ -105,7 +105,14 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i
|
||||
try:
|
||||
await measurements.update()
|
||||
except (AirlyError, ClientConnectorError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
|
||||
_LOGGER.debug(
|
||||
"Requests remaining: %s/%s",
|
||||
@ -126,7 +133,11 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i
|
||||
standards = measurements.current["standards"]
|
||||
|
||||
if index["description"] == NO_AIRLY_SENSORS:
|
||||
raise UpdateFailed("Can't retrieve data: no Airly sensors in this area")
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_station",
|
||||
translation_placeholders={"entry": self.config_entry.title},
|
||||
)
|
||||
for value in values:
|
||||
data[value["name"]] = value["value"]
|
||||
for standard in standards:
|
||||
|
@ -36,5 +36,13 @@
|
||||
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the Airly API for {entry}: {error}"
|
||||
},
|
||||
"no_station": {
|
||||
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -351,7 +351,7 @@ class BackupManager:
|
||||
|
||||
# Latest backup event and backup event subscribers
|
||||
self.last_event: ManagerStateEvent = BlockedEvent()
|
||||
self.last_non_idle_event: ManagerStateEvent | None = None
|
||||
self.last_action_event: ManagerStateEvent | None = None
|
||||
self._backup_event_subscriptions = hass.data[
|
||||
DATA_BACKUP
|
||||
].backup_event_subscriptions
|
||||
@ -1337,7 +1337,7 @@ class BackupManager:
|
||||
LOGGER.debug("Backup state: %s -> %s", current_state, new_state)
|
||||
self.last_event = event
|
||||
if not isinstance(event, (BlockedEvent, IdleEvent)):
|
||||
self.last_non_idle_event = event
|
||||
self.last_action_event = event
|
||||
for subscription in self._backup_event_subscriptions:
|
||||
subscription(event)
|
||||
|
||||
|
@ -55,7 +55,7 @@ async def handle_info(
|
||||
"backups": list(backups.values()),
|
||||
"last_attempted_automatic_backup": manager.config.data.last_attempted_automatic_backup,
|
||||
"last_completed_automatic_backup": manager.config.data.last_completed_automatic_backup,
|
||||
"last_non_idle_event": manager.last_non_idle_event,
|
||||
"last_action_event": manager.last_action_event,
|
||||
"next_automatic_backup": manager.config.data.schedule.next_automatic_backup,
|
||||
"next_automatic_backup_additional": manager.config.data.schedule.next_automatic_backup_additional,
|
||||
"state": manager.state,
|
||||
|
@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.26.1",
|
||||
"dbus-fast==2.39.5",
|
||||
"habluetooth==3.27.0"
|
||||
"dbus-fast==2.39.6",
|
||||
"habluetooth==3.32.0"
|
||||
]
|
||||
}
|
||||
|
@ -4,10 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_EMAIL, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import BringConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_NAME, CONF_EMAIL}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: BringConfigEntry
|
||||
@ -15,7 +19,10 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"data": {k: v.to_dict() for k, v in config_entry.runtime_data.data.items()},
|
||||
"data": {
|
||||
k: async_redact_data(v.to_dict(), TO_REDACT)
|
||||
for k, v in config_entry.runtime_data.data.items()
|
||||
},
|
||||
"lists": [lst.to_dict() for lst in config_entry.runtime_data.lists],
|
||||
"user_settings": config_entry.runtime_data.user_settings.to_dict(),
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bring_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bring-api==1.0.2"]
|
||||
"requirements": ["bring-api==1.1.0"]
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BrotherConfigEntry, BrotherDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@ -25,7 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b
|
||||
host, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
)
|
||||
except (ConnectionError, SnmpError, TimeoutError) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"device": entry.title,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
|
||||
coordinator = BrotherDataUpdateCoordinator(hass, entry, brother)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
@ -26,6 +26,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.brother = brother
|
||||
self.device_name = config_entry.title
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
@ -41,5 +42,12 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
|
||||
async with timeout(20):
|
||||
data = await self.brother.async_update()
|
||||
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"device": self.device_name,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
return data
|
||||
|
@ -159,5 +159,13 @@
|
||||
"name": "Last restart"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while connecting to the {device} printer: {error}"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the {device} printer: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"ffmpeg_arguments": "Arguments passed to ffmpeg for cameras",
|
||||
"timeout": "Request Timeout (seconds)"
|
||||
"ffmpeg_arguments": "Arguments passed to FFmpeg for cameras",
|
||||
"timeout": "Request timeout (seconds)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,12 +16,21 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_UUID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, DOMAIN
|
||||
|
||||
IGNORE_CEC_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
|
||||
KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
|
||||
KNOWN_HOSTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_KNOWN_HOSTS,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(custom_value=True, options=[], multiple=True),
|
||||
)
|
||||
}
|
||||
)
|
||||
WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
|
||||
|
||||
|
||||
@ -30,12 +39,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self._ignore_cec = set[str]()
|
||||
self._known_hosts = set[str]()
|
||||
self._wanted_uuid = set[str]()
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@ -62,48 +65,31 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup."""
|
||||
errors = {}
|
||||
data = {CONF_KNOWN_HOSTS: self._known_hosts}
|
||||
|
||||
if user_input is not None:
|
||||
bad_hosts = False
|
||||
known_hosts = user_input[CONF_KNOWN_HOSTS]
|
||||
known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()]
|
||||
try:
|
||||
known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts)
|
||||
except vol.Invalid:
|
||||
errors["base"] = "invalid_known_hosts"
|
||||
bad_hosts = True
|
||||
else:
|
||||
self._known_hosts = known_hosts
|
||||
data = self._get_data()
|
||||
if not bad_hosts:
|
||||
return self.async_create_entry(title="Google Cast", data=data)
|
||||
known_hosts = _trim_items(user_input.get(CONF_KNOWN_HOSTS, []))
|
||||
return self.async_create_entry(
|
||||
title="Google Cast",
|
||||
data=self._get_data(known_hosts=known_hosts),
|
||||
)
|
||||
|
||||
fields = {}
|
||||
fields[vol.Optional(CONF_KNOWN_HOSTS, default="")] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="config", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
return self.async_show_form(step_id="config", data_schema=KNOWN_HOSTS_SCHEMA)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup."""
|
||||
|
||||
data = self._get_data()
|
||||
|
||||
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
|
||||
return self.async_create_entry(title="Google Cast", data=data)
|
||||
return self.async_create_entry(title="Google Cast", data=self._get_data())
|
||||
|
||||
return self.async_show_form(step_id="confirm")
|
||||
|
||||
def _get_data(self):
|
||||
def _get_data(
|
||||
self, *, known_hosts: list[str] | None = None
|
||||
) -> dict[str, list[str]]:
|
||||
return {
|
||||
CONF_IGNORE_CEC: list(self._ignore_cec),
|
||||
CONF_KNOWN_HOSTS: list(self._known_hosts),
|
||||
CONF_UUID: list(self._wanted_uuid),
|
||||
CONF_IGNORE_CEC: [],
|
||||
CONF_KNOWN_HOSTS: known_hosts or [],
|
||||
CONF_UUID: [],
|
||||
}
|
||||
|
||||
|
||||
@ -123,31 +109,24 @@ class CastOptionsFlowHandler(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Google Cast options."""
|
||||
errors: dict[str, str] = {}
|
||||
current_config = self.config_entry.data
|
||||
if user_input is not None:
|
||||
bad_hosts, known_hosts = _string_to_list(
|
||||
user_input.get(CONF_KNOWN_HOSTS, ""), KNOWN_HOSTS_SCHEMA
|
||||
known_hosts = _trim_items(user_input.get(CONF_KNOWN_HOSTS, []))
|
||||
self.updated_config = dict(self.config_entry.data)
|
||||
self.updated_config[CONF_KNOWN_HOSTS] = known_hosts
|
||||
|
||||
if self.show_advanced_options:
|
||||
return await self.async_step_advanced_options()
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=self.updated_config
|
||||
)
|
||||
|
||||
if not bad_hosts:
|
||||
self.updated_config = dict(current_config)
|
||||
self.updated_config[CONF_KNOWN_HOSTS] = known_hosts
|
||||
|
||||
if self.show_advanced_options:
|
||||
return await self.async_step_advanced_options()
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data=self.updated_config
|
||||
)
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
fields: dict[vol.Marker, type[str]] = {}
|
||||
suggested_value = _list_to_string(current_config.get(CONF_KNOWN_HOSTS))
|
||||
_add_with_suggestion(fields, CONF_KNOWN_HOSTS, suggested_value)
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="basic_options",
|
||||
data_schema=vol.Schema(fields),
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
KNOWN_HOSTS_SCHEMA, self.config_entry.data
|
||||
),
|
||||
errors=errors,
|
||||
last_step=not self.show_advanced_options,
|
||||
)
|
||||
@ -206,6 +185,10 @@ def _string_to_list(string, schema):
|
||||
return invalid, items
|
||||
|
||||
|
||||
def _trim_items(items: list[str]) -> list[str]:
|
||||
return [x.strip() for x in items if x.strip()]
|
||||
|
||||
|
||||
def _add_with_suggestion(
|
||||
fields: dict[vol.Marker, type[str]], key: str, suggested_value: str
|
||||
) -> None:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
from typing import TYPE_CHECKING, NotRequired, TypedDict
|
||||
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
@ -46,3 +46,4 @@ class HomeAssistantControllerData(TypedDict):
|
||||
hass_uuid: str
|
||||
client_id: str | None
|
||||
refresh_token: str
|
||||
app_id: NotRequired[str]
|
||||
|
@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
import aiohttp
|
||||
import attr
|
||||
@ -40,7 +41,7 @@ class ChromecastInfo:
|
||||
is_dynamic_group = attr.ib(type=bool | None, default=None)
|
||||
|
||||
@property
|
||||
def friendly_name(self) -> str:
|
||||
def friendly_name(self) -> str | None:
|
||||
"""Return the Friendly Name."""
|
||||
return self.cast_info.friendly_name
|
||||
|
||||
@ -50,7 +51,7 @@ class ChromecastInfo:
|
||||
return self.cast_info.cast_type == CAST_TYPE_GROUP
|
||||
|
||||
@property
|
||||
def uuid(self) -> bool:
|
||||
def uuid(self) -> UUID:
|
||||
"""Return the UUID."""
|
||||
return self.cast_info.uuid
|
||||
|
||||
@ -111,7 +112,10 @@ class ChromecastInfo:
|
||||
is_dynamic_group = False
|
||||
http_group_status = None
|
||||
http_group_status = dial.get_multizone_status(
|
||||
None,
|
||||
# We pass services which will be used for the HTTP request, and we
|
||||
# don't care about the host in http_group_status.dynamic_groups so
|
||||
# we pass an empty string to simplify the code.
|
||||
"",
|
||||
services=self.cast_info.services,
|
||||
zconf=ChromeCastZeroconf.get_zeroconf(),
|
||||
)
|
||||
|
@ -14,7 +14,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.5"],
|
||||
"requirements": ["PyChromecast==14.0.6"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ show_lovelace_view:
|
||||
integration: cast
|
||||
domain: media_player
|
||||
dashboard_path:
|
||||
required: true
|
||||
example: lovelace-cast
|
||||
selector:
|
||||
text:
|
||||
view_path:
|
||||
required: true
|
||||
example: downstairs
|
||||
selector:
|
||||
text:
|
||||
|
@ -6,9 +6,11 @@
|
||||
},
|
||||
"config": {
|
||||
"title": "Google Cast configuration",
|
||||
"description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.",
|
||||
"data": {
|
||||
"known_hosts": "Known hosts"
|
||||
"known_hosts": "Add known host"
|
||||
},
|
||||
"data_description": {
|
||||
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -20,9 +22,11 @@
|
||||
"step": {
|
||||
"basic_options": {
|
||||
"title": "[%key:component::cast::config::step::config::title%]",
|
||||
"description": "[%key:component::cast::config::step::config::description%]",
|
||||
"data": {
|
||||
"known_hosts": "[%key:component::cast::config::step::config::data::known_hosts%]"
|
||||
},
|
||||
"data_description": {
|
||||
"known_hosts": "[%key:component::cast::config::step::config::data_description::known_hosts%]"
|
||||
}
|
||||
},
|
||||
"advanced_options": {
|
||||
@ -49,7 +53,7 @@
|
||||
},
|
||||
"dashboard_path": {
|
||||
"name": "Dashboard path",
|
||||
"description": "The URL path of the dashboard to show."
|
||||
"description": "The URL path of the dashboard to show, defaults to lovelace if not specified."
|
||||
},
|
||||
"view_path": {
|
||||
"name": "View path",
|
||||
|
@ -51,8 +51,7 @@ def async_get_chat_log(
|
||||
)
|
||||
if user_input is not None and (
|
||||
(content := chat_log.content[-1]).role != "user"
|
||||
# MyPy doesn't understand that content is a UserContent here
|
||||
or content.content != user_input.text # type: ignore[union-attr]
|
||||
or content.content != user_input.text
|
||||
):
|
||||
chat_log.async_add_user_content(UserContent(content=user_input.text))
|
||||
|
||||
@ -128,7 +127,7 @@ class ConverseError(HomeAssistantError):
|
||||
class SystemContent:
|
||||
"""Base class for chat messages."""
|
||||
|
||||
role: str = field(init=False, default="system")
|
||||
role: Literal["system"] = field(init=False, default="system")
|
||||
content: str
|
||||
|
||||
|
||||
@ -136,7 +135,7 @@ class SystemContent:
|
||||
class UserContent:
|
||||
"""Assistant content."""
|
||||
|
||||
role: str = field(init=False, default="user")
|
||||
role: Literal["user"] = field(init=False, default="user")
|
||||
content: str
|
||||
|
||||
|
||||
@ -144,7 +143,7 @@ class UserContent:
|
||||
class AssistantContent:
|
||||
"""Assistant content."""
|
||||
|
||||
role: str = field(init=False, default="assistant")
|
||||
role: Literal["assistant"] = field(init=False, default="assistant")
|
||||
agent_id: str
|
||||
content: str | None = None
|
||||
tool_calls: list[llm.ToolInput] | None = None
|
||||
@ -154,7 +153,7 @@ class AssistantContent:
|
||||
class ToolResultContent:
|
||||
"""Tool result content."""
|
||||
|
||||
role: str = field(init=False, default="tool_result")
|
||||
role: Literal["tool_result"] = field(init=False, default="tool_result")
|
||||
agent_id: str
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
@ -193,8 +192,8 @@ class ChatLog:
|
||||
|
||||
return (
|
||||
last_msg.role == "assistant"
|
||||
and last_msg.content is not None # type: ignore[union-attr]
|
||||
and last_msg.content.strip().endswith( # type: ignore[union-attr]
|
||||
and last_msg.content is not None
|
||||
and last_msg.content.strip().endswith(
|
||||
(
|
||||
"?",
|
||||
";", # Greek question mark
|
||||
|
@ -101,9 +101,11 @@ def hostname_from_url(url: str) -> str:
|
||||
|
||||
def _host_validator(config: dict[str, str]) -> dict[str, str]:
|
||||
"""Validate that a host is properly configured."""
|
||||
if config[CONF_HOST].startswith("elks://"):
|
||||
if config[CONF_HOST].startswith(("elks://", "elksv1_2://")):
|
||||
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
|
||||
raise vol.Invalid("Specify username and password for elks://")
|
||||
raise vol.Invalid(
|
||||
"Specify username and password for elks:// or elksv1_2://"
|
||||
)
|
||||
elif not config[CONF_HOST].startswith("elk://") and not config[
|
||||
CONF_HOST
|
||||
].startswith("serial://"):
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Energenie Power Sockets Integration.",
|
||||
"title": "Energenie Power Sockets",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
|
@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.6.0",
|
||||
"aioesphomeapi==29.7.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
@ -54,7 +54,7 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"timeout": "Request timeout (seconds)",
|
||||
"ffmpeg_arguments": "Arguments passed to ffmpeg for cameras"
|
||||
"ffmpeg_arguments": "Arguments passed to FFmpeg for cameras"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"services": {
|
||||
"restart": {
|
||||
"name": "[%key:common::action::restart%]",
|
||||
"description": "Sends a restart command to a ffmpeg based sensor.",
|
||||
"description": "Sends a restart command to an FFmpeg-based sensor.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@ -12,7 +12,7 @@
|
||||
},
|
||||
"start": {
|
||||
"name": "[%key:common::action::start%]",
|
||||
"description": "Sends a start command to a ffmpeg based sensor.",
|
||||
"description": "Sends a start command to an FFmpeg-based sensor.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"stop": {
|
||||
"name": "[%key:common::action::stop%]",
|
||||
"description": "Sends a stop command to a ffmpeg based sensor.",
|
||||
"description": "Sends a stop command to an FFmpeg-based sensor.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
|
@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["fyta_cli"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["fyta_cli==0.7.1"]
|
||||
"requirements": ["fyta_cli==0.7.2"]
|
||||
}
|
||||
|
@ -44,7 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: GiosConfigEntry) -> bool
|
||||
try:
|
||||
gios = await Gios.create(websession, station_id)
|
||||
except (GiosError, ConnectionError, ClientConnectorError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"entry": entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
|
||||
coordinator = GiosDataUpdateCoordinator(hass, entry, gios)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
@ -57,4 +57,11 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]):
|
||||
async with asyncio.timeout(API_TIMEOUT):
|
||||
return await self.gios.async_update()
|
||||
except (GiosError, ClientConnectorError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(error),
|
||||
},
|
||||
) from error
|
||||
|
@ -170,5 +170,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while connecting to the GIOS API for {entry}: {error}"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the GIOS API for {entry}: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==8.3.0"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.1"]
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
@ -83,7 +84,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
if not Path(filename).exists():
|
||||
raise HomeAssistantError(f"`{filename}` does not exist")
|
||||
prompt_parts.append(client.files.upload(file=filename))
|
||||
mimetype = mimetypes.guess_type(filename)[0]
|
||||
with open(filename, "rb") as file:
|
||||
uploaded_file = client.files.upload(
|
||||
file=file, config={"mime_type": mimetype}
|
||||
)
|
||||
prompt_parts.append(uploaded_file)
|
||||
|
||||
await hass.async_add_executor_job(append_files_to_prompt)
|
||||
|
||||
|
@ -188,7 +188,7 @@ def _convert_content(
|
||||
| conversation.SystemContent,
|
||||
) -> Content:
|
||||
"""Convert HA content to Google content."""
|
||||
if content.role != "assistant" or not content.tool_calls: # type: ignore[union-attr]
|
||||
if content.role != "assistant" or not content.tool_calls:
|
||||
role = "model" if content.role == "assistant" else content.role
|
||||
return Content(
|
||||
role=role,
|
||||
@ -321,24 +321,14 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
for chat_content in chat_log.content[1:-1]:
|
||||
if chat_content.role == "tool_result":
|
||||
# mypy doesn't like picking a type based on checking shared property 'role'
|
||||
tool_results.append(cast(conversation.ToolResultContent, chat_content))
|
||||
tool_results.append(chat_content)
|
||||
continue
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
tool_results.clear()
|
||||
|
||||
messages.append(
|
||||
_convert_content(
|
||||
cast(
|
||||
conversation.UserContent
|
||||
| conversation.SystemContent
|
||||
| conversation.AssistantContent,
|
||||
chat_content,
|
||||
)
|
||||
)
|
||||
)
|
||||
messages.append(_convert_content(chat_content))
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "google_generative_ai_conversation",
|
||||
"name": "Google Generative AI",
|
||||
"after_dependencies": ["assist_pipeline", "intent"],
|
||||
"codeowners": ["@tronikos"],
|
||||
"codeowners": ["@tronikos", "@ivanlh"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["conversation"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
|
||||
|
@ -38,28 +38,28 @@
|
||||
"name": "Input 1 voltage"
|
||||
},
|
||||
"inverter_amperage_input_1": {
|
||||
"name": "Input 1 Amperage"
|
||||
"name": "Input 1 amperage"
|
||||
},
|
||||
"inverter_wattage_input_1": {
|
||||
"name": "Input 1 Wattage"
|
||||
"name": "Input 1 wattage"
|
||||
},
|
||||
"inverter_voltage_input_2": {
|
||||
"name": "Input 2 voltage"
|
||||
},
|
||||
"inverter_amperage_input_2": {
|
||||
"name": "Input 2 Amperage"
|
||||
"name": "Input 2 amperage"
|
||||
},
|
||||
"inverter_wattage_input_2": {
|
||||
"name": "Input 2 Wattage"
|
||||
"name": "Input 2 wattage"
|
||||
},
|
||||
"inverter_voltage_input_3": {
|
||||
"name": "Input 3 voltage"
|
||||
},
|
||||
"inverter_amperage_input_3": {
|
||||
"name": "Input 3 Amperage"
|
||||
"name": "Input 3 amperage"
|
||||
},
|
||||
"inverter_wattage_input_3": {
|
||||
"name": "Input 3 Wattage"
|
||||
"name": "Input 3 wattage"
|
||||
},
|
||||
"inverter_internal_wattage": {
|
||||
"name": "Internal wattage"
|
||||
@ -137,13 +137,13 @@
|
||||
"name": "Load consumption"
|
||||
},
|
||||
"mix_wattage_pv_1": {
|
||||
"name": "PV1 Wattage"
|
||||
"name": "PV1 wattage"
|
||||
},
|
||||
"mix_wattage_pv_2": {
|
||||
"name": "PV2 Wattage"
|
||||
"name": "PV2 wattage"
|
||||
},
|
||||
"mix_wattage_pv_all": {
|
||||
"name": "All PV Wattage"
|
||||
"name": "All PV wattage"
|
||||
},
|
||||
"mix_export_to_grid": {
|
||||
"name": "Export to grid"
|
||||
@ -182,7 +182,7 @@
|
||||
"name": "Storage production today"
|
||||
},
|
||||
"storage_storage_production_lifetime": {
|
||||
"name": "Lifetime Storage production"
|
||||
"name": "Lifetime storage production"
|
||||
},
|
||||
"storage_grid_discharge_today": {
|
||||
"name": "Grid discharged today"
|
||||
@ -224,7 +224,7 @@
|
||||
"name": "Storage charging/ discharging(-ve)"
|
||||
},
|
||||
"storage_load_consumption_solar_storage": {
|
||||
"name": "Load consumption (Solar + Storage)"
|
||||
"name": "Load consumption (solar + storage)"
|
||||
},
|
||||
"storage_charge_today": {
|
||||
"name": "Charge today"
|
||||
@ -257,7 +257,7 @@
|
||||
"name": "Output voltage"
|
||||
},
|
||||
"storage_ac_output_frequency": {
|
||||
"name": "Ac output frequency"
|
||||
"name": "AC output frequency"
|
||||
},
|
||||
"storage_current_pv": {
|
||||
"name": "Solar charge current"
|
||||
@ -290,7 +290,7 @@
|
||||
"name": "Lifetime total energy input 1"
|
||||
},
|
||||
"tlx_energy_today_input_1": {
|
||||
"name": "Energy Today Input 1"
|
||||
"name": "Energy today input 1"
|
||||
},
|
||||
"tlx_voltage_input_1": {
|
||||
"name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_1::name%]"
|
||||
@ -305,7 +305,7 @@
|
||||
"name": "Lifetime total energy input 2"
|
||||
},
|
||||
"tlx_energy_today_input_2": {
|
||||
"name": "Energy Today Input 2"
|
||||
"name": "Energy today input 2"
|
||||
},
|
||||
"tlx_voltage_input_2": {
|
||||
"name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_2::name%]"
|
||||
@ -320,7 +320,7 @@
|
||||
"name": "Lifetime total energy input 3"
|
||||
},
|
||||
"tlx_energy_today_input_3": {
|
||||
"name": "Energy Today Input 3"
|
||||
"name": "Energy today input 3"
|
||||
},
|
||||
"tlx_voltage_input_3": {
|
||||
"name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_3::name%]"
|
||||
@ -335,16 +335,16 @@
|
||||
"name": "Lifetime total energy input 4"
|
||||
},
|
||||
"tlx_energy_today_input_4": {
|
||||
"name": "Energy Today Input 4"
|
||||
"name": "Energy today input 4"
|
||||
},
|
||||
"tlx_voltage_input_4": {
|
||||
"name": "Input 4 voltage"
|
||||
},
|
||||
"tlx_amperage_input_4": {
|
||||
"name": "Input 4 Amperage"
|
||||
"name": "Input 4 amperage"
|
||||
},
|
||||
"tlx_wattage_input_4": {
|
||||
"name": "Input 4 Wattage"
|
||||
"name": "Input 4 wattage"
|
||||
},
|
||||
"tlx_solar_generation_total": {
|
||||
"name": "Lifetime total solar energy"
|
||||
@ -434,10 +434,10 @@
|
||||
"name": "Money lifetime"
|
||||
},
|
||||
"total_energy_today": {
|
||||
"name": "Energy Today"
|
||||
"name": "Energy today"
|
||||
},
|
||||
"total_output_power": {
|
||||
"name": "Output Power"
|
||||
"name": "Output power"
|
||||
},
|
||||
"total_energy_output": {
|
||||
"name": "[%key:component::growatt_server::entity::sensor::inverter_energy_total::name%]"
|
||||
|
@ -768,7 +768,7 @@
|
||||
"description": "[%key:component::habitica::common::notes_description%]"
|
||||
},
|
||||
"tag": {
|
||||
"name": "[%key:component::habitica::common::tag_name%]",
|
||||
"name": "[%key:component::habitica::common::tag_options_name%]",
|
||||
"description": "[%key:component::habitica::common::tag_description%]"
|
||||
},
|
||||
"alias": {
|
||||
@ -868,7 +868,7 @@
|
||||
"description": "[%key:component::habitica::common::notes_description%]"
|
||||
},
|
||||
"tag": {
|
||||
"name": "[%key:component::habitica::common::tag_name%]",
|
||||
"name": "[%key:component::habitica::common::tag_options_name%]",
|
||||
"description": "[%key:component::habitica::common::tag_description%]"
|
||||
},
|
||||
"alias": {
|
||||
@ -1008,7 +1008,7 @@
|
||||
"description": "[%key:component::habitica::common::notes_description%]"
|
||||
},
|
||||
"tag": {
|
||||
"name": "[%key:component::habitica::common::tag_name%]",
|
||||
"name": "[%key:component::habitica::common::tag_options_name%]",
|
||||
"description": "[%key:component::habitica::common::tag_description%]"
|
||||
},
|
||||
"alias": {
|
||||
@ -1024,11 +1024,11 @@
|
||||
"description": "[%key:component::habitica::common::date_description%]"
|
||||
},
|
||||
"reminder": {
|
||||
"name": "[%key:component::habitica::common::reminder_name%]",
|
||||
"name": "[%key:component::habitica::common::reminder_options_name%]",
|
||||
"description": "[%key:component::habitica::common::reminder_description%]"
|
||||
},
|
||||
"add_checklist_item": {
|
||||
"name": "[%key:component::habitica::common::add_checklist_item_name%]",
|
||||
"name": "[%key:component::habitica::common::checklist_options_name%]",
|
||||
"description": "[%key:component::habitica::common::add_checklist_item_description%]"
|
||||
}
|
||||
},
|
||||
|
@ -3,27 +3,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from functools import reduce, wraps
|
||||
import logging
|
||||
from operator import ior
|
||||
from typing import Any
|
||||
from typing import Any, Final
|
||||
|
||||
from pyheos import (
|
||||
AddCriteriaType,
|
||||
ControlType,
|
||||
HeosError,
|
||||
HeosPlayer,
|
||||
MediaItem,
|
||||
MediaMusicSource,
|
||||
MediaType as HeosMediaType,
|
||||
PlayState,
|
||||
RepeatType,
|
||||
const as heos_const,
|
||||
)
|
||||
from pyheos.util import mediauri as heos_source
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaPlayerEnqueue,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
@ -32,6 +40,7 @@ from homeassistant.components.media_player import (
|
||||
RepeatMode,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.components.media_source import BrowseMediaSource
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
@ -55,6 +64,8 @@ from .coordinator import HeosConfigEntry, HeosCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
BROWSE_ROOT: Final = "heos://media"
|
||||
|
||||
BASE_SUPPORTED_FEATURES = (
|
||||
MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
@ -97,6 +108,21 @@ HEOS_HA_REPEAT_TYPE_MAP = {
|
||||
}
|
||||
HA_HEOS_REPEAT_TYPE_MAP = {v: k for k, v in HEOS_HA_REPEAT_TYPE_MAP.items()}
|
||||
|
||||
HEOS_MEDIA_TYPE_TO_MEDIA_CLASS = {
|
||||
HeosMediaType.ALBUM: MediaClass.ALBUM,
|
||||
HeosMediaType.ARTIST: MediaClass.ARTIST,
|
||||
HeosMediaType.CONTAINER: MediaClass.DIRECTORY,
|
||||
HeosMediaType.GENRE: MediaClass.GENRE,
|
||||
HeosMediaType.HEOS_SERVER: MediaClass.DIRECTORY,
|
||||
HeosMediaType.HEOS_SERVICE: MediaClass.DIRECTORY,
|
||||
HeosMediaType.MUSIC_SERVICE: MediaClass.DIRECTORY,
|
||||
HeosMediaType.PLAYLIST: MediaClass.PLAYLIST,
|
||||
HeosMediaType.SONG: MediaClass.TRACK,
|
||||
HeosMediaType.STATION: MediaClass.TRACK,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -282,6 +308,16 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
if heos_source.is_media_uri(media_id):
|
||||
media, data = heos_source.from_media_uri(media_id)
|
||||
if not isinstance(media, MediaItem):
|
||||
raise ValueError(f"Invalid media id '{media_id}'")
|
||||
await self._player.play_media(
|
||||
media,
|
||||
HA_HEOS_ENQUEUE_MAP[kwargs.get(ATTR_MEDIA_ENQUEUE)],
|
||||
)
|
||||
return
|
||||
|
||||
if media_source.is_media_source_id(media_id):
|
||||
media_type = MediaType.URL
|
||||
play_item = await media_source.async_resolve_media(
|
||||
@ -534,14 +570,101 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._player.volume / 100
|
||||
|
||||
async def _async_browse_media_root(self) -> BrowseMedia:
|
||||
"""Return media browsing root."""
|
||||
if not self.coordinator.heos.music_sources:
|
||||
try:
|
||||
await self.coordinator.heos.get_music_sources()
|
||||
except HeosError as error:
|
||||
_LOGGER.debug("Unable to load music sources: %s", error)
|
||||
children: list[BrowseMedia] = [
|
||||
_media_to_browse_media(source)
|
||||
for source in self.coordinator.heos.music_sources.values()
|
||||
if source.available
|
||||
]
|
||||
root = BrowseMedia(
|
||||
title="Music Sources",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
media_content_type="",
|
||||
media_content_id=BROWSE_ROOT,
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
children=children,
|
||||
)
|
||||
# Append media source items
|
||||
with suppress(BrowseError):
|
||||
browse = await self._async_browse_media_source()
|
||||
# If domain is None, it's an overview of available sources
|
||||
if browse.domain is None and browse.children:
|
||||
children.extend(browse.children)
|
||||
else:
|
||||
children.append(browse)
|
||||
return root
|
||||
|
||||
async def _async_browse_heos_media(self, media_content_id: str) -> BrowseMedia:
|
||||
"""Browse a HEOS media item."""
|
||||
media, data = heos_source.from_media_uri(media_content_id)
|
||||
browse_media = _media_to_browse_media(media)
|
||||
try:
|
||||
browse_result = await self.coordinator.heos.browse_media(media)
|
||||
except HeosError as error:
|
||||
_LOGGER.debug("Unable to browse media %s: %s", media, error)
|
||||
else:
|
||||
browse_media.children = [
|
||||
_media_to_browse_media(item)
|
||||
for item in browse_result.items
|
||||
if item.browsable or item.playable
|
||||
]
|
||||
return browse_media
|
||||
|
||||
async def _async_browse_media_source(
|
||||
self, media_content_id: str | None = None
|
||||
) -> BrowseMediaSource:
|
||||
"""Browse a media source item."""
|
||||
return await media_source.async_browse_media(
|
||||
self.hass,
|
||||
media_content_id,
|
||||
content_filter=lambda item: item.media_content_type.startswith("audio/"),
|
||||
)
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""Implement the websocket media browsing helper."""
|
||||
return await media_source.async_browse_media(
|
||||
self.hass,
|
||||
media_content_id,
|
||||
content_filter=lambda item: item.media_content_type.startswith("audio/"),
|
||||
if media_content_id in (None, BROWSE_ROOT):
|
||||
return await self._async_browse_media_root()
|
||||
assert media_content_id is not None
|
||||
if heos_source.is_media_uri(media_content_id):
|
||||
return await self._async_browse_heos_media(media_content_id)
|
||||
if media_source.is_media_source_id(media_content_id):
|
||||
return await self._async_browse_media_source(media_content_id)
|
||||
raise ServiceValidationError(
|
||||
translation_domain=HEOS_DOMAIN,
|
||||
translation_key="unsupported_media_content_id",
|
||||
translation_placeholders={"media_content_id": media_content_id},
|
||||
)
|
||||
|
||||
|
||||
def _media_to_browse_media(media: MediaItem | MediaMusicSource) -> BrowseMedia:
|
||||
"""Convert a HEOS media item to a browse media item."""
|
||||
can_expand = False
|
||||
can_play = False
|
||||
|
||||
if isinstance(media, MediaMusicSource):
|
||||
can_expand = media.available
|
||||
else:
|
||||
can_expand = media.browsable
|
||||
can_play = media.playable
|
||||
|
||||
return BrowseMedia(
|
||||
can_expand=can_expand,
|
||||
can_play=can_play,
|
||||
media_content_id=heos_source.to_media_uri(media),
|
||||
media_content_type="",
|
||||
media_class=HEOS_MEDIA_TYPE_TO_MEDIA_CLASS[media.type],
|
||||
title=media.name,
|
||||
thumbnail=media.image_url,
|
||||
)
|
||||
|
@ -146,6 +146,9 @@
|
||||
},
|
||||
"unknown_source": {
|
||||
"message": "Unknown source: {source}"
|
||||
},
|
||||
"unsupported_media_content_id": {
|
||||
"message": "Unsupported media_content_id: {media_content_id}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
@ -629,14 +629,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
|
||||
home_connect_client = HomeConnectClient(config_entry_auth)
|
||||
|
||||
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
await coordinator.async_setup()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.runtime_data.start_event_listener()
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
coordinator.async_refresh(),
|
||||
f"home_connect-initial-full-refresh-{entry.entry_id}",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -102,7 +102,7 @@ class HomeConnectButtonEntity(HomeConnectEntity, ButtonEntity):
|
||||
)
|
||||
self.entity_description = desc
|
||||
self.appliance = appliance
|
||||
self.unique_id = f"{appliance.info.ha_id}-{desc.key}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc.key}"
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
|
@ -137,41 +137,6 @@ def setup_home_connect_entry(
|
||||
defaultdict(list)
|
||||
)
|
||||
|
||||
entities: list[HomeConnectEntity] = []
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities_to_add = get_entities_for_appliance(entry, appliance)
|
||||
if get_option_entities_for_appliance:
|
||||
entities_to_add.extend(get_option_entities_for_appliance(entry, appliance))
|
||||
for event_key in (
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
):
|
||||
changed_options_listener_remove_callback = (
|
||||
entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_create_option_entities,
|
||||
entry,
|
||||
appliance,
|
||||
known_entity_unique_ids,
|
||||
get_option_entities_for_appliance,
|
||||
async_add_entities,
|
||||
),
|
||||
(appliance.info.ha_id, event_key),
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(changed_options_listener_remove_callback)
|
||||
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
|
||||
changed_options_listener_remove_callback
|
||||
)
|
||||
known_entity_unique_ids.update(
|
||||
{
|
||||
cast(str, entity.unique_id): appliance.info.ha_id
|
||||
for entity in entities_to_add
|
||||
}
|
||||
)
|
||||
entities.extend(entities_to_add)
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
|
@ -10,6 +10,7 @@ from .utils import bsh_key_to_translation_key
|
||||
|
||||
DOMAIN = "home_connect"
|
||||
|
||||
API_DEFAULT_RETRY_AFTER = 60
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"CleaningRobot",
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import sleep as asyncio_sleep
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
@ -29,6 +29,7 @@ from aiohomeconnect.model.error import (
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
HomeConnectRequestError,
|
||||
TooManyRequestsError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
|
||||
@ -36,11 +37,11 @@ from propcache.api import cached_property
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
|
||||
from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -154,7 +155,7 @@ class HomeConnectCoordinator(
|
||||
f"home_connect-events_listener_task-{self.config_entry.entry_id}",
|
||||
)
|
||||
|
||||
async def _event_listener(self) -> None:
|
||||
async def _event_listener(self) -> None: # noqa: C901
|
||||
"""Match event with listener for event type."""
|
||||
retry_time = 10
|
||||
while True:
|
||||
@ -231,15 +232,15 @@ class HomeConnectCoordinator(
|
||||
self.data[event_message_ha_id].update(appliance_data)
|
||||
else:
|
||||
self.data[event_message_ha_id] = appliance_data
|
||||
for listener, context in list(
|
||||
self._special_listeners.values()
|
||||
) + list(self._listeners.values()):
|
||||
assert isinstance(context, tuple)
|
||||
for listener, context in self._special_listeners.values():
|
||||
if (
|
||||
EventKey.BSH_COMMON_APPLIANCE_DEPAIRED
|
||||
not in context
|
||||
):
|
||||
listener()
|
||||
self._call_all_event_listeners_for_appliance(
|
||||
event_message_ha_id
|
||||
)
|
||||
|
||||
case EventType.DISCONNECTED:
|
||||
self.data[event_message_ha_id].info.connected = False
|
||||
@ -269,7 +270,7 @@ class HomeConnectCoordinator(
|
||||
error,
|
||||
retry_time,
|
||||
)
|
||||
await asyncio.sleep(retry_time)
|
||||
await asyncio_sleep(retry_time)
|
||||
retry_time = min(retry_time * 2, 3600)
|
||||
except HomeConnectApiError as error:
|
||||
_LOGGER.error("Error while listening for events: %s", error)
|
||||
@ -278,6 +279,13 @@ class HomeConnectCoordinator(
|
||||
)
|
||||
break
|
||||
|
||||
# Trigger to delete the possible depaired device entities
|
||||
# from known_entities variable at common.py
|
||||
for listener, context in self._special_listeners.values():
|
||||
assert isinstance(context, tuple)
|
||||
if EventKey.BSH_COMMON_APPLIANCE_DEPAIRED in context:
|
||||
listener()
|
||||
|
||||
@callback
|
||||
def _call_event_listener(self, event_message: EventMessage) -> None:
|
||||
"""Call listener for event."""
|
||||
@ -295,6 +303,42 @@ class HomeConnectCoordinator(
|
||||
|
||||
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
|
||||
"""Fetch data from Home Connect."""
|
||||
await self._async_setup()
|
||||
|
||||
for appliance_data in self.data.values():
|
||||
appliance = appliance_data.info
|
||||
ha_id = appliance.ha_id
|
||||
while True:
|
||||
try:
|
||||
self.data[ha_id] = await self._get_appliance_data(
|
||||
appliance, self.data.get(ha_id)
|
||||
)
|
||||
except TooManyRequestsError as err:
|
||||
_LOGGER.debug(
|
||||
"Rate limit exceeded on initial fetch: %s",
|
||||
err,
|
||||
)
|
||||
await asyncio_sleep(err.retry_after or API_DEFAULT_RETRY_AFTER)
|
||||
else:
|
||||
break
|
||||
|
||||
for listener, context in self._special_listeners.values():
|
||||
assert isinstance(context, tuple)
|
||||
if EventKey.BSH_COMMON_APPLIANCE_PAIRED in context:
|
||||
listener()
|
||||
|
||||
return self.data
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the devices."""
|
||||
try:
|
||||
await self._async_setup()
|
||||
except UpdateFailed as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the devices."""
|
||||
old_appliances = set(self.data.keys())
|
||||
try:
|
||||
appliances = await self.client.get_home_appliances()
|
||||
except UnauthorizedError as error:
|
||||
@ -312,12 +356,38 @@ class HomeConnectCoordinator(
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
|
||||
return {
|
||||
appliance.ha_id: await self._get_appliance_data(
|
||||
appliance, self.data.get(appliance.ha_id)
|
||||
for appliance in appliances.homeappliances:
|
||||
self.device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={(DOMAIN, appliance.ha_id)},
|
||||
manufacturer=appliance.brand,
|
||||
name=appliance.name,
|
||||
model=appliance.vib,
|
||||
)
|
||||
for appliance in appliances.homeappliances
|
||||
}
|
||||
if appliance.ha_id not in self.data:
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
else:
|
||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||
old_appliances.remove(appliance.ha_id)
|
||||
|
||||
for ha_id in old_appliances:
|
||||
self.data.pop(ha_id, None)
|
||||
device = self.device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, ha_id)}
|
||||
)
|
||||
if device:
|
||||
self.device_registry.async_update_device(
|
||||
device_id=device.id,
|
||||
remove_config_entry_id=self.config_entry.entry_id,
|
||||
)
|
||||
|
||||
async def _get_appliance_data(
|
||||
self,
|
||||
@ -339,6 +409,8 @@ class HomeConnectCoordinator(
|
||||
await self.client.get_settings(appliance.ha_id)
|
||||
).settings
|
||||
}
|
||||
except TooManyRequestsError:
|
||||
raise
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching settings for %s: %s",
|
||||
@ -351,6 +423,8 @@ class HomeConnectCoordinator(
|
||||
status.key: status
|
||||
for status in (await self.client.get_status(appliance.ha_id)).status
|
||||
}
|
||||
except TooManyRequestsError:
|
||||
raise
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching status for %s: %s",
|
||||
@ -365,6 +439,8 @@ class HomeConnectCoordinator(
|
||||
if appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
try:
|
||||
all_programs = await self.client.get_all_programs(appliance.ha_id)
|
||||
except TooManyRequestsError:
|
||||
raise
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching programs for %s: %s",
|
||||
@ -421,6 +497,8 @@ class HomeConnectCoordinator(
|
||||
await self.client.get_available_commands(appliance.ha_id)
|
||||
).commands
|
||||
}
|
||||
except TooManyRequestsError:
|
||||
raise
|
||||
except HomeConnectError:
|
||||
commands = set()
|
||||
|
||||
@ -455,6 +533,8 @@ class HomeConnectCoordinator(
|
||||
).options
|
||||
or []
|
||||
}
|
||||
except TooManyRequestsError:
|
||||
raise
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching options for %s: %s",
|
||||
|
@ -1,21 +1,28 @@
|
||||
"""Home Connect entity base class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import Any, Concatenate, cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey
|
||||
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
|
||||
from aiohomeconnect.model.error import (
|
||||
ActiveProgramNotSetError,
|
||||
HomeConnectError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
def bsh_key(self) -> OptionKey:
|
||||
"""Return the BSH key."""
|
||||
return cast(OptionKey, self.entity_description.key)
|
||||
|
||||
|
||||
def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate the function to catch Home Connect too many requests error and retry later.
|
||||
|
||||
If it needs to be called later, it will call async_write_ha_state function
|
||||
"""
|
||||
|
||||
async def handler_to_return(
|
||||
self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> None:
|
||||
async def handler(_datetime: datetime | None = None) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except TooManyRequestsError as err:
|
||||
if (retry_after := err.retry_after) is None:
|
||||
retry_after = API_DEFAULT_RETRY_AFTER
|
||||
async_call_later(self.hass, retry_after, handler)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error(
|
||||
"Error fetching constraints for %s: %s", self.entity_id, err
|
||||
)
|
||||
else:
|
||||
if _datetime is not None:
|
||||
self.async_write_ha_state()
|
||||
|
||||
await handler()
|
||||
|
||||
return handler_to_return
|
||||
|
@ -25,7 +25,7 @@ from .const import (
|
||||
UNIT_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
},
|
||||
) from err
|
||||
|
||||
@constraint_fetcher
|
||||
async def async_fetch_constraints(self) -> None:
|
||||
"""Fetch the max and min values and step for the number entity."""
|
||||
try:
|
||||
setting_key = cast(SettingKey, self.bsh_key)
|
||||
data = self.appliance.settings.get(setting_key)
|
||||
if not data or not data.unit or not data.constraints:
|
||||
data = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key)
|
||||
self.appliance.info.ha_id, setting_key=setting_key
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("An error occurred: %s", err)
|
||||
else:
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = data.unit
|
||||
self.set_constraints(data)
|
||||
|
||||
def set_constraints(self, setting: GetSetting) -> None:
|
||||
"""Set constraints for the number entity."""
|
||||
if setting.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(
|
||||
setting.unit, setting.unit
|
||||
)
|
||||
if not (constraints := setting.constraints):
|
||||
return
|
||||
if constraints.max:
|
||||
@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||
self._attr_native_unit_of_measurement = data.unit
|
||||
self.set_constraints(data)
|
||||
if (
|
||||
not hasattr(self, "_attr_native_min_value")
|
||||
not hasattr(self, "_attr_native_unit_of_measurement")
|
||||
or not hasattr(self, "_attr_native_min_value")
|
||||
or not hasattr(self, "_attr_native_max_value")
|
||||
or not hasattr(self, "_attr_native_step")
|
||||
):
|
||||
@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
|
||||
or candidate_unit != self._attr_native_unit_of_measurement
|
||||
):
|
||||
self._attr_native_unit_of_measurement = candidate_unit
|
||||
self.__dict__.pop("unit_of_measurement", None)
|
||||
option_constraints = option_definition.constraints
|
||||
if option_constraints:
|
||||
if (
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Provides a select platform for Home Connect."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
@ -47,9 +47,11 @@ from .coordinator import (
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
||||
@ -413,6 +415,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
"""Select setting class for Home Connect."""
|
||||
|
||||
entity_description: HomeConnectSelectEntityDescription
|
||||
_original_option_keys: set[str | None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -421,6 +424,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
desc: HomeConnectSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._original_option_keys = set(desc.values_translation_key)
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
@ -458,23 +462,29 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self.async_fetch_options()
|
||||
|
||||
@constraint_fetcher
|
||||
async def async_fetch_options(self) -> None:
|
||||
"""Fetch options from the API."""
|
||||
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
|
||||
if (
|
||||
not setting
|
||||
or not setting.constraints
|
||||
or not setting.constraints.allowed_values
|
||||
):
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
setting = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
)
|
||||
setting = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
)
|
||||
|
||||
if setting and setting.constraints and setting.constraints.allowed_values:
|
||||
self._original_option_keys = set(setting.constraints.allowed_values)
|
||||
self._attr_options = [
|
||||
self.entity_description.values_translation_key[option]
|
||||
for option in setting.constraints.allowed_values
|
||||
if option in self.entity_description.values_translation_key
|
||||
for option in self._original_option_keys
|
||||
if option is not None
|
||||
and option in self.entity_description.values_translation_key
|
||||
]
|
||||
|
||||
|
||||
@ -491,7 +501,7 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
|
||||
desc: HomeConnectSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._original_option_keys = set(desc.values_translation_key.keys())
|
||||
self._original_option_keys = set(desc.values_translation_key)
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
@ -524,5 +534,5 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
|
||||
self.entity_description.values_translation_key[option]
|
||||
for option in self._original_option_keys
|
||||
if option is not None
|
||||
and option in self.entity_description.values_translation_key
|
||||
]
|
||||
self.__dict__.pop("options", None)
|
||||
|
@ -1,12 +1,11 @@
|
||||
"""Provides a sensor for Home Connect."""
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, StatusKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -28,7 +27,9 @@ from .const import (
|
||||
UNIT_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
from .entity import HomeConnectEntity, constraint_fetcher
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
||||
else:
|
||||
await self.fetch_unit()
|
||||
|
||||
@constraint_fetcher
|
||||
async def fetch_unit(self) -> None:
|
||||
"""Fetch the unit of measurement."""
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
data = await self.coordinator.client.get_status_value(
|
||||
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
||||
)
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(
|
||||
data.unit, data.unit
|
||||
)
|
||||
data = await self.coordinator.client.get_status_value(
|
||||
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
|
||||
)
|
||||
if data.unit:
|
||||
self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
|
||||
|
||||
|
||||
class HomeConnectProgramSensor(HomeConnectSensor):
|
||||
|
@ -468,11 +468,11 @@ set_program_and_options:
|
||||
translation_key: venting_level
|
||||
options:
|
||||
- cooking_hood_enum_type_stage_fan_off
|
||||
- cooking_hood_enum_type_stage_fan_stage01
|
||||
- cooking_hood_enum_type_stage_fan_stage02
|
||||
- cooking_hood_enum_type_stage_fan_stage03
|
||||
- cooking_hood_enum_type_stage_fan_stage04
|
||||
- cooking_hood_enum_type_stage_fan_stage05
|
||||
- cooking_hood_enum_type_stage_fan_stage_01
|
||||
- cooking_hood_enum_type_stage_fan_stage_02
|
||||
- cooking_hood_enum_type_stage_fan_stage_03
|
||||
- cooking_hood_enum_type_stage_fan_stage_04
|
||||
- cooking_hood_enum_type_stage_fan_stage_05
|
||||
cooking_hood_option_intensive_level:
|
||||
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
required: false
|
||||
@ -528,7 +528,7 @@ set_program_and_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
laundry_care_washer_option_temperature:
|
||||
example: laundry_care_washer_enum_type_temperature_g_c40
|
||||
example: laundry_care_washer_enum_type_temperature_g_c_40
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@ -536,14 +536,14 @@ set_program_and_options:
|
||||
translation_key: washer_temperature
|
||||
options:
|
||||
- laundry_care_washer_enum_type_temperature_cold
|
||||
- laundry_care_washer_enum_type_temperature_g_c20
|
||||
- laundry_care_washer_enum_type_temperature_g_c30
|
||||
- laundry_care_washer_enum_type_temperature_g_c40
|
||||
- laundry_care_washer_enum_type_temperature_g_c50
|
||||
- laundry_care_washer_enum_type_temperature_g_c60
|
||||
- laundry_care_washer_enum_type_temperature_g_c70
|
||||
- laundry_care_washer_enum_type_temperature_g_c80
|
||||
- laundry_care_washer_enum_type_temperature_g_c90
|
||||
- laundry_care_washer_enum_type_temperature_g_c_20
|
||||
- laundry_care_washer_enum_type_temperature_g_c_30
|
||||
- laundry_care_washer_enum_type_temperature_g_c_40
|
||||
- laundry_care_washer_enum_type_temperature_g_c_50
|
||||
- laundry_care_washer_enum_type_temperature_g_c_60
|
||||
- laundry_care_washer_enum_type_temperature_g_c_70
|
||||
- laundry_care_washer_enum_type_temperature_g_c_80
|
||||
- laundry_care_washer_enum_type_temperature_g_c_90
|
||||
- laundry_care_washer_enum_type_temperature_ul_cold
|
||||
- laundry_care_washer_enum_type_temperature_ul_warm
|
||||
- laundry_care_washer_enum_type_temperature_ul_hot
|
||||
@ -557,15 +557,15 @@ set_program_and_options:
|
||||
translation_key: spin_speed
|
||||
options:
|
||||
- laundry_care_washer_enum_type_spin_speed_off
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m600
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m700
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m900
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1000
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1200
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1600
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_600
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_700
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_800
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_900
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_1000
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_1200
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_1400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m_1600
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_off
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_low
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_medium
|
||||
|
@ -417,11 +417,11 @@
|
||||
"venting_level": {
|
||||
"options": {
|
||||
"cooking_hood_enum_type_stage_fan_off": "Fan off",
|
||||
"cooking_hood_enum_type_stage_fan_stage01": "Fan stage 1",
|
||||
"cooking_hood_enum_type_stage_fan_stage02": "Fan stage 2",
|
||||
"cooking_hood_enum_type_stage_fan_stage03": "Fan stage 3",
|
||||
"cooking_hood_enum_type_stage_fan_stage04": "Fan stage 4",
|
||||
"cooking_hood_enum_type_stage_fan_stage05": "Fan stage 5"
|
||||
"cooking_hood_enum_type_stage_fan_stage_01": "Fan stage 1",
|
||||
"cooking_hood_enum_type_stage_fan_stage_02": "Fan stage 2",
|
||||
"cooking_hood_enum_type_stage_fan_stage_03": "Fan stage 3",
|
||||
"cooking_hood_enum_type_stage_fan_stage_04": "Fan stage 4",
|
||||
"cooking_hood_enum_type_stage_fan_stage_05": "Fan stage 5"
|
||||
}
|
||||
},
|
||||
"intensive_level": {
|
||||
@ -441,14 +441,14 @@
|
||||
"washer_temperature": {
|
||||
"options": {
|
||||
"laundry_care_washer_enum_type_temperature_cold": "Cold",
|
||||
"laundry_care_washer_enum_type_temperature_g_c20": "20ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c30": "30ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c40": "40ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c50": "50ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c60": "60ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c70": "70ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c80": "80ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c90": "90ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_20": "20ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_30": "30ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_40": "40ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_50": "50ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_60": "60ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_70": "70ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_80": "80ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_90": "90ºC clothes",
|
||||
"laundry_care_washer_enum_type_temperature_ul_cold": "Cold",
|
||||
"laundry_care_washer_enum_type_temperature_ul_warm": "Warm",
|
||||
"laundry_care_washer_enum_type_temperature_ul_hot": "Hot",
|
||||
@ -458,15 +458,15 @@
|
||||
"spin_speed": {
|
||||
"options": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m700": "700 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "900 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "1600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_800": "800 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_900": "900 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "1000 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
|
||||
@ -1384,11 +1384,11 @@
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage01%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage02%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage03%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage04%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage05%]"
|
||||
"cooking_hood_enum_type_stage_fan_stage_01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_01%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage_02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_02%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage_03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_03%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage_04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_04%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage_05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_05%]"
|
||||
}
|
||||
},
|
||||
"intensive_level": {
|
||||
@ -1411,14 +1411,14 @@
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_temperature_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_cold%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c20%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c30%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c40%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c50%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c60%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c70%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c80%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c90%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_20%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_30%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_40%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_50%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_60%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_70%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_80%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c_90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_90%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_cold%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_warm": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_warm%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_hot%]",
|
||||
@ -1429,15 +1429,15 @@
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m700%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m900%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_700%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_800%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_900%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1000%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1200%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
||||
|
@ -188,7 +188,7 @@
|
||||
},
|
||||
"reload_all": {
|
||||
"name": "Reload all",
|
||||
"description": "Reload all YAML configuration that can be reloaded without restarting Home Assistant."
|
||||
"description": "Reloads all YAML configuration that can be reloaded without restarting Home Assistant."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@ -15,6 +15,7 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
|
190
homeassistant/components/homee/binary_sensor.py
Normal file
190
homeassistant/components/homee/binary_sensor.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""The Homee binary sensor platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS: dict[AttributeType, BinarySensorEntityDescription] = {
|
||||
AttributeType.BATTERY_LOW_ALARM: BinarySensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.BLACKOUT_ALARM: BinarySensorEntityDescription(
|
||||
key="blackout_alarm",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.COALARM: BinarySensorEntityDescription(
|
||||
key="carbon_monoxide", device_class=BinarySensorDeviceClass.CO
|
||||
),
|
||||
AttributeType.CO2ALARM: BinarySensorEntityDescription(
|
||||
key="carbon_dioxide", device_class=BinarySensorDeviceClass.PROBLEM
|
||||
),
|
||||
AttributeType.FLOOD_ALARM: BinarySensorEntityDescription(
|
||||
key="flood",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
AttributeType.HIGH_TEMPERATURE_ALARM: BinarySensorEntityDescription(
|
||||
key="high_temperature",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.LEAK_ALARM: BinarySensorEntityDescription(
|
||||
key="leak_alarm",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
),
|
||||
AttributeType.LOAD_ALARM: BinarySensorEntityDescription(
|
||||
key="load_alarm",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.LOCK_STATE: BinarySensorEntityDescription(
|
||||
key="lock",
|
||||
device_class=BinarySensorDeviceClass.LOCK,
|
||||
),
|
||||
AttributeType.LOW_TEMPERATURE_ALARM: BinarySensorEntityDescription(
|
||||
key="low_temperature",
|
||||
device_class=BinarySensorDeviceClass.COLD,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.MALFUNCTION_ALARM: BinarySensorEntityDescription(
|
||||
key="malfunction",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.MAXIMUM_ALARM: BinarySensorEntityDescription(
|
||||
key="maximum",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.MINIMUM_ALARM: BinarySensorEntityDescription(
|
||||
key="minimum",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.MOTION_ALARM: BinarySensorEntityDescription(
|
||||
key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
),
|
||||
AttributeType.MOTOR_BLOCKED_ALARM: BinarySensorEntityDescription(
|
||||
key="motor_blocked",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.ON_OFF: BinarySensorEntityDescription(
|
||||
key="plug",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
),
|
||||
AttributeType.OPEN_CLOSE: BinarySensorEntityDescription(
|
||||
key="opening",
|
||||
device_class=BinarySensorDeviceClass.OPENING,
|
||||
),
|
||||
AttributeType.OVER_CURRENT_ALARM: BinarySensorEntityDescription(
|
||||
key="overcurrent",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.OVERLOAD_ALARM: BinarySensorEntityDescription(
|
||||
key="overload",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.PRESENCE_ALARM: BinarySensorEntityDescription(
|
||||
key="presence",
|
||||
device_class=BinarySensorDeviceClass.PRESENCE,
|
||||
),
|
||||
AttributeType.POWER_SUPPLY_ALARM: BinarySensorEntityDescription(
|
||||
key="power",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.RAIN_FALL: BinarySensorEntityDescription(
|
||||
key="rain",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
AttributeType.REPLACE_FILTER_ALARM: BinarySensorEntityDescription(
|
||||
key="replace_filter",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.SMOKE_ALARM: BinarySensorEntityDescription(
|
||||
key="smoke",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
),
|
||||
AttributeType.STORAGE_ALARM: BinarySensorEntityDescription(
|
||||
key="storage",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.SURGE_ALARM: BinarySensorEntityDescription(
|
||||
key="surge",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.TAMPER_ALARM: BinarySensorEntityDescription(
|
||||
key="tamper",
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.VOLTAGE_DROP_ALARM: BinarySensorEntityDescription(
|
||||
key="voltage_drop",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.WATER_ALARM: BinarySensorEntityDescription(
|
||||
key="water",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the binary sensor component."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeBinarySensor(
|
||||
attribute, config_entry, BINARY_SENSOR_DESCRIPTIONS[attribute.type]
|
||||
)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BINARY_SENSOR_DESCRIPTIONS and not attribute.editable
|
||||
)
|
||||
|
||||
|
||||
class HomeeBinarySensor(HomeeEntity, BinarySensorEntity):
|
||||
"""Representation of a Homee binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute: HomeeAttribute,
|
||||
entry: HomeeConfigEntry,
|
||||
description: BinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Homee binary sensor entity."""
|
||||
super().__init__(attribute, entry)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_translation_key = description.key
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return bool(self._attribute.current_value)
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyHomee==1.2.7"]
|
||||
"requirements": ["pyHomee==1.2.8"]
|
||||
}
|
||||
|
@ -26,6 +26,76 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"blackout_alarm": {
|
||||
"name": "Blackout"
|
||||
},
|
||||
"carbon_dioxide": {
|
||||
"name": "Carbon dioxide"
|
||||
},
|
||||
"flood": {
|
||||
"name": "Flood"
|
||||
},
|
||||
"high_temperature": {
|
||||
"name": "High temperature"
|
||||
},
|
||||
"leak_alarm": {
|
||||
"name": "Leak"
|
||||
},
|
||||
"load_alarm": {
|
||||
"name": "Load",
|
||||
"state": {
|
||||
"off": "Normal",
|
||||
"on": "Overload"
|
||||
}
|
||||
},
|
||||
"low_temperature": {
|
||||
"name": "Low temperature"
|
||||
},
|
||||
"malfunction": {
|
||||
"name": "Malfunction"
|
||||
},
|
||||
"maximum": {
|
||||
"name": "Maximum level"
|
||||
},
|
||||
"minimum": {
|
||||
"name": "Minimum level"
|
||||
},
|
||||
"motor_blocked": {
|
||||
"name": "Motor blocked"
|
||||
},
|
||||
"overcurrent": {
|
||||
"name": "Overcurrent"
|
||||
},
|
||||
"overload": {
|
||||
"name": "Overload"
|
||||
},
|
||||
"rain": {
|
||||
"name": "Rain"
|
||||
},
|
||||
"replace_filter": {
|
||||
"name": "Replace filter",
|
||||
"state": {
|
||||
"on": "Replace"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"name": "Storage",
|
||||
"state": {
|
||||
"off": "Space available",
|
||||
"on": "Storage full"
|
||||
}
|
||||
},
|
||||
"surge": {
|
||||
"name": "Surge"
|
||||
},
|
||||
"voltage_drop": {
|
||||
"name": "Voltage drop"
|
||||
},
|
||||
"water": {
|
||||
"name": "Water"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"automatic_mode": {
|
||||
"name": "Automatic mode"
|
||||
|
@ -83,7 +83,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
features = MediaPlayerEntityFeature(0)
|
||||
features = MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON
|
||||
|
||||
if self.service.has(CharacteristicsTypes.ACTIVE_IDENTIFIER):
|
||||
features |= MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
@ -177,6 +177,14 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity):
|
||||
|
||||
return MediaPlayerState.ON
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the tv on."""
|
||||
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: 1})
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the tv off."""
|
||||
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: 0})
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state == MediaPlayerState.PLAYING:
|
||||
|
@ -35,7 +35,7 @@
|
||||
"services": {
|
||||
"activate_eco_mode_with_duration": {
|
||||
"name": "Activate eco mode with duration",
|
||||
"description": "Activates eco mode with period.",
|
||||
"description": "Activates the eco mode for a specified duration.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
@ -49,7 +49,7 @@
|
||||
},
|
||||
"activate_eco_mode_with_period": {
|
||||
"name": "Activate eco more with period",
|
||||
"description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::description%]",
|
||||
"description": "Activates the eco mode until a given time.",
|
||||
"fields": {
|
||||
"endtime": {
|
||||
"name": "Endtime",
|
||||
@ -63,7 +63,7 @@
|
||||
},
|
||||
"activate_vacation": {
|
||||
"name": "Activate vacation",
|
||||
"description": "Activates the vacation mode until the given time.",
|
||||
"description": "Activates the vacation mode until a given time.",
|
||||
"fields": {
|
||||
"endtime": {
|
||||
"name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_period::fields::endtime::name%]",
|
||||
|
@ -89,7 +89,7 @@
|
||||
"fields": {
|
||||
"mode": {
|
||||
"name": "Mode",
|
||||
"description": "Operation mode. For example, _normal_, _eco_, or _away_. For a list of possible values, refer to the integration documentation."
|
||||
"description": "Operation mode. For example, \"normal\", \"eco\", or \"away\". For a list of possible values, refer to the integration documentation."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ from aioautomower.exceptions import (
|
||||
HusqvarnaTimeoutError,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerAttributes
|
||||
from aioautomower.model import MowerDictionary
|
||||
from aioautomower.session import AutomowerSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -32,7 +32,7 @@ DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
||||
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
|
||||
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
"""Class to manage fetching Husqvarna data."""
|
||||
|
||||
config_entry: AutomowerConfigEntry
|
||||
@ -61,7 +61,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
self._zones_last_update: dict[str, set[str]] = {}
|
||||
self._areas_last_update: dict[str, set[int]] = {}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, MowerAttributes]:
|
||||
async def _async_update_data(self) -> MowerDictionary:
|
||||
"""Subscribe for websocket and poll data from the API."""
|
||||
if not self.ws_connected:
|
||||
await self.api.connect()
|
||||
@ -84,7 +84,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
return data
|
||||
|
||||
@callback
|
||||
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
|
||||
def callback(self, ws_data: MowerDictionary) -> None:
|
||||
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
||||
self.async_set_updated_data(ws_data)
|
||||
|
||||
@ -119,7 +119,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
"reconnect_task",
|
||||
)
|
||||
|
||||
def _async_add_remove_devices(self, data: dict[str, MowerAttributes]) -> None:
|
||||
def _async_add_remove_devices(self, data: MowerDictionary) -> None:
|
||||
"""Add new device, remove non-existing device."""
|
||||
current_devices = set(data)
|
||||
|
||||
@ -159,9 +159,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
for mower_callback in self.new_devices_callbacks:
|
||||
mower_callback(new_devices)
|
||||
|
||||
def _async_add_remove_stay_out_zones(
|
||||
self, data: dict[str, MowerAttributes]
|
||||
) -> None:
|
||||
def _async_add_remove_stay_out_zones(self, data: MowerDictionary) -> None:
|
||||
"""Add new stay-out zones, remove non-existing stay-out zones."""
|
||||
current_zones = {
|
||||
mower_id: set(mower_data.stay_out_zones.zones)
|
||||
@ -207,7 +205,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
|
||||
return current_zones
|
||||
|
||||
def _async_add_remove_work_areas(self, data: dict[str, MowerAttributes]) -> None:
|
||||
def _async_add_remove_work_areas(self, data: MowerDictionary) -> None:
|
||||
"""Add new work areas, remove non-existing work areas."""
|
||||
current_areas = {
|
||||
mower_id: set(mower_data.work_areas)
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.1.1"]
|
||||
"requirements": ["aioautomower==2025.3.1"]
|
||||
}
|
||||
|
@ -38,7 +38,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ImgwPibConfigEntry) -> b
|
||||
hydrological_details=False,
|
||||
)
|
||||
except (ClientError, TimeoutError, ApiError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"entry": entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
|
||||
coordinator = ImgwPibDataUpdateCoordinator(hass, entry, imgwpib, station_id)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
@ -63,4 +63,11 @@ class ImgwPibDataUpdateCoordinator(DataUpdateCoordinator[HydrologicalData]):
|
||||
try:
|
||||
return await self.imgwpib.get_hydrological_data()
|
||||
except ApiError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
|
@ -25,5 +25,13 @@
|
||||
"name": "Water temperature"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while connecting to the IMGW-PIB API for {entry}: {error}"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the IMGW-PIB API for {entry}: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ from iometer import IOmeterClient, IOmeterConnectionError, Reading, Status
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -31,6 +32,7 @@ class IOMeterCoordinator(DataUpdateCoordinator[IOmeterData]):
|
||||
|
||||
config_entry: IOmeterConfigEntry
|
||||
client: IOmeterClient
|
||||
current_fw_version: str = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -58,4 +60,17 @@ class IOMeterCoordinator(DataUpdateCoordinator[IOmeterData]):
|
||||
except IOmeterConnectionError as error:
|
||||
raise UpdateFailed(f"Error communicating with IOmeter: {error}") from error
|
||||
|
||||
fw_version = f"{status.device.core.version}/{status.device.bridge.version}"
|
||||
if self.current_fw_version and fw_version != self.current_fw_version:
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, status.device.id)}
|
||||
)
|
||||
assert device_entry
|
||||
device_registry.async_update_device(
|
||||
device_entry.id,
|
||||
sw_version=fw_version,
|
||||
)
|
||||
self.current_fw_version = fw_version
|
||||
|
||||
return IOmeterData(reading=reading, status=status)
|
||||
|
@ -20,5 +20,5 @@ class IOmeterEntity(CoordinatorEntity[IOMeterCoordinator]):
|
||||
identifiers={(DOMAIN, status.device.id)},
|
||||
manufacturer="IOmeter GmbH",
|
||||
model="IOmeter",
|
||||
sw_version=f"{status.device.core.version}/{status.device.bridge.version}",
|
||||
sw_version=coordinator.current_fw_version,
|
||||
)
|
||||
|
@ -4,7 +4,7 @@
|
||||
"user": {
|
||||
"description": "Fill out your U.S. or Canadian ZIP code.",
|
||||
"data": {
|
||||
"zip_code": "ZIP Code"
|
||||
"zip_code": "ZIP code"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -12,7 +12,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.6.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.1.30.194235"
|
||||
"knx-frontend==2025.3.8.214559"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
),
|
||||
vol.Optional(CONF_RESET_AFTER): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0, max=10, step=0.1, unit_of_measurement="s"
|
||||
min=0, max=600, step=0.1, unit_of_measurement="s"
|
||||
)
|
||||
),
|
||||
},
|
||||
|
@ -61,6 +61,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
client=client,
|
||||
)
|
||||
|
||||
# initialize the firmware update coordinator early to check the firmware version
|
||||
firmware_device = LaMarzoccoMachine(
|
||||
model=entry.data[CONF_MODEL],
|
||||
serial_number=entry.unique_id,
|
||||
name=entry.data[CONF_NAME],
|
||||
cloud_client=cloud_client,
|
||||
)
|
||||
|
||||
firmware_coordinator = LaMarzoccoFirmwareUpdateCoordinator(
|
||||
hass, entry, firmware_device
|
||||
)
|
||||
await firmware_coordinator.async_config_entry_first_refresh()
|
||||
gateway_version = version.parse(
|
||||
firmware_device.firmware[FirmwareType.GATEWAY].current_version
|
||||
)
|
||||
|
||||
if gateway_version >= version.parse("v5.0.9"):
|
||||
# remove host from config entry, it is not supported anymore
|
||||
data = {k: v for k, v in entry.data.items() if k != CONF_HOST}
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=data,
|
||||
)
|
||||
|
||||
elif gateway_version < version.parse("v3.4-rc5"):
|
||||
# incompatible gateway firmware, create an issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"unsupported_gateway_firmware",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="unsupported_gateway_firmware",
|
||||
translation_placeholders={"gateway_version": str(gateway_version)},
|
||||
)
|
||||
|
||||
# initialize local API
|
||||
local_client: LaMarzoccoLocalClient | None = None
|
||||
if (host := entry.data.get(CONF_HOST)) is not None:
|
||||
@ -117,30 +153,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
|
||||
coordinators = LaMarzoccoRuntimeData(
|
||||
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
|
||||
LaMarzoccoFirmwareUpdateCoordinator(hass, entry, device),
|
||||
firmware_coordinator,
|
||||
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
|
||||
)
|
||||
|
||||
# API does not like concurrent requests, so no asyncio.gather here
|
||||
await coordinators.config_coordinator.async_config_entry_first_refresh()
|
||||
await coordinators.firmware_coordinator.async_config_entry_first_refresh()
|
||||
await coordinators.statistics_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinators
|
||||
|
||||
gateway_version = device.firmware[FirmwareType.GATEWAY].current_version
|
||||
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
|
||||
# incompatible gateway firmware, create an issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"unsupported_gateway_firmware",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="unsupported_gateway_firmware",
|
||||
translation_placeholders={"gateway_version": gateway_version},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def update_listener(
|
||||
|
@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==1.4.7"]
|
||||
"requirements": ["pylamarzocco==1.4.9"]
|
||||
}
|
||||
|
@ -144,9 +144,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
|
||||
set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
|
||||
prebrew_off_time=value, key=key
|
||||
),
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[key][
|
||||
0
|
||||
].off_time,
|
||||
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
|
||||
and device.config.prebrew_mode == PrebrewMode.PREBREW,
|
||||
and device.config.prebrew_mode
|
||||
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
|
||||
supported_fn=lambda coordinator: coordinator.device.model
|
||||
!= MachineModel.GS3_MP,
|
||||
),
|
||||
@ -162,9 +165,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
|
||||
set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
|
||||
prebrew_on_time=value, key=key
|
||||
),
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time,
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[key][
|
||||
0
|
||||
].off_time,
|
||||
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
|
||||
and device.config.prebrew_mode == PrebrewMode.PREBREW,
|
||||
and device.config.prebrew_mode
|
||||
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
|
||||
supported_fn=lambda coordinator: coordinator.device.model
|
||||
!= MachineModel.GS3_MP,
|
||||
),
|
||||
@ -180,8 +186,8 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
|
||||
set_value_fn=lambda machine, value, key: machine.set_preinfusion_time(
|
||||
preinfusion_time=value, key=key
|
||||
),
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[
|
||||
key
|
||||
native_value_fn=lambda config, key: config.prebrew_configuration[key][
|
||||
1
|
||||
].preinfusion_time,
|
||||
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
|
||||
and device.config.prebrew_mode == PrebrewMode.PREINFUSION,
|
||||
|
@ -38,6 +38,7 @@ STEAM_LEVEL_LM_TO_HA = {value: key for key, value in STEAM_LEVEL_HA_TO_LM.items(
|
||||
PREBREW_MODE_HA_TO_LM = {
|
||||
"disabled": PrebrewMode.DISABLED,
|
||||
"prebrew": PrebrewMode.PREBREW,
|
||||
"prebrew_enabled": PrebrewMode.PREBREW_ENABLED,
|
||||
"preinfusion": PrebrewMode.PREINFUSION,
|
||||
}
|
||||
|
||||
|
@ -148,6 +148,7 @@
|
||||
"state": {
|
||||
"disabled": "Disabled",
|
||||
"prebrew": "Prebrew",
|
||||
"prebrew_enabled": "Prebrew",
|
||||
"preinfusion": "Preinfusion"
|
||||
}
|
||||
},
|
||||
|
@ -396,19 +396,19 @@
|
||||
},
|
||||
"address_to_device_id": {
|
||||
"name": "Address to device ID",
|
||||
"description": "Convert LCN address to device ID.",
|
||||
"description": "Converts an LCN address into a device ID.",
|
||||
"fields": {
|
||||
"id": {
|
||||
"name": "Module or group ID",
|
||||
"description": "Target module or group ID."
|
||||
"description": "Module or group number of the target."
|
||||
},
|
||||
"segment_id": {
|
||||
"name": "Segment ID",
|
||||
"description": "Target segment ID."
|
||||
"description": "Segment number of the target."
|
||||
},
|
||||
"type": {
|
||||
"name": "Type",
|
||||
"description": "Target type."
|
||||
"description": "Module type of the target."
|
||||
},
|
||||
"host": {
|
||||
"name": "Host name",
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["thinqconnect"],
|
||||
"requirements": ["thinqconnect==1.0.4"]
|
||||
"requirements": ["thinqconnect==1.0.5"]
|
||||
}
|
||||
|
@ -77,31 +77,31 @@
|
||||
"status_code": {
|
||||
"name": "Status code",
|
||||
"state": {
|
||||
"br": "Bonnet Removed",
|
||||
"ccc": "Clean Cycle Complete",
|
||||
"ccp": "Clean Cycle In Progress",
|
||||
"cd": "Cat Detected",
|
||||
"csf": "Cat Sensor Fault",
|
||||
"csi": "Cat Sensor Interrupted",
|
||||
"cst": "Cat Sensor Timing",
|
||||
"df1": "Drawer Almost Full - 2 Cycles Left",
|
||||
"df2": "Drawer Almost Full - 1 Cycle Left",
|
||||
"dfs": "Drawer Full",
|
||||
"dhf": "Dump + Home Position Fault",
|
||||
"dpf": "Dump Position Fault",
|
||||
"ec": "Empty Cycle",
|
||||
"hpf": "Home Position Fault",
|
||||
"br": "Bonnet removed",
|
||||
"ccc": "Clean cycle complete",
|
||||
"ccp": "Clean cycle in progress",
|
||||
"cd": "Cat detected",
|
||||
"csf": "Cat sensor fault",
|
||||
"csi": "Cat sensor interrupted",
|
||||
"cst": "Cat sensor timing",
|
||||
"df1": "Drawer almost full - 2 cycles left",
|
||||
"df2": "Drawer almost full - 1 cycle left",
|
||||
"dfs": "Drawer full",
|
||||
"dhf": "Dump + home position fault",
|
||||
"dpf": "Dump position fault",
|
||||
"ec": "Empty cycle",
|
||||
"hpf": "Home position fault",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"offline": "Offline",
|
||||
"otf": "Over Torque Fault",
|
||||
"otf": "Over torque fault",
|
||||
"p": "[%key:common::state::paused%]",
|
||||
"pd": "Pinch Detect",
|
||||
"pwrd": "Powering Down",
|
||||
"pwru": "Powering Up",
|
||||
"pd": "Pinch detect",
|
||||
"pwrd": "Powering down",
|
||||
"pwru": "Powering up",
|
||||
"rdy": "Ready",
|
||||
"scf": "Cat Sensor Fault At Startup",
|
||||
"sdf": "Drawer Full At Startup",
|
||||
"spf": "Pinch Detect At Startup"
|
||||
"scf": "Cat sensor fault at startup",
|
||||
"sdf": "Drawer full at startup",
|
||||
"spf": "Pinch detect at startup"
|
||||
}
|
||||
},
|
||||
"waste_drawer": {
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==8.3.0"]
|
||||
"requirements": ["ical==9.0.1"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==8.3.0"]
|
||||
"requirements": ["ical==9.0.1"]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import http, llm_api
|
||||
from . import http
|
||||
from .const import DOMAIN
|
||||
from .session import SessionManager
|
||||
from .types import MCPServerConfigEntry
|
||||
@ -25,7 +25,6 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Model Context Protocol component."""
|
||||
http.async_register(hass)
|
||||
llm_api.async_register_api(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LLM_API, LLM_API_NAME
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -33,13 +33,6 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)}
|
||||
if LLM_API not in llm_apis:
|
||||
# MCP server component is not loaded yet, so make the LLM API a choice.
|
||||
llm_apis = {
|
||||
LLM_API: LLM_API_NAME,
|
||||
**llm_apis,
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
|
||||
|
@ -2,5 +2,6 @@
|
||||
|
||||
DOMAIN = "mcp_server"
|
||||
TITLE = "Model Context Protocol Server"
|
||||
LLM_API = "stateless_assist"
|
||||
LLM_API_NAME = "Stateless Assist"
|
||||
# The Stateless API is no longer registered explicitly, but this name may still exist in the
|
||||
# users config entry.
|
||||
STATELESS_LLM_API = "stateless_assist"
|
||||
|
@ -1,19 +1,18 @@
|
||||
"""LLM API for MCP Server."""
|
||||
"""LLM API for MCP Server.
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
This is a modified version of the AssistAPI that does not include the home state
|
||||
in the prompt. This API is not registered with the LLM API registry since it is
|
||||
only used by the MCP Server. The MCP server will substitute this API when the
|
||||
user selects the Assist API.
|
||||
"""
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.util import yaml as yaml_util
|
||||
|
||||
from .const import LLM_API, LLM_API_NAME
|
||||
|
||||
EXPOSED_ENTITY_FIELDS = {"name", "domain", "description", "areas", "names"}
|
||||
|
||||
|
||||
def async_register_api(hass: HomeAssistant) -> None:
|
||||
"""Register the LLM API."""
|
||||
llm.async_register_api(hass, StatelessAssistAPI(hass))
|
||||
|
||||
|
||||
class StatelessAssistAPI(llm.AssistAPI):
|
||||
"""LLM API for MCP Server that provides the Assist API without state information in the prompt.
|
||||
|
||||
@ -22,12 +21,6 @@ class StatelessAssistAPI(llm.AssistAPI):
|
||||
actions don't care about the current state, there is little quality loss.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the StatelessAssistAPI."""
|
||||
super().__init__(hass)
|
||||
self.id = LLM_API
|
||||
self.name = LLM_API_NAME
|
||||
|
||||
@callback
|
||||
def _async_get_exposed_entities_prompt(
|
||||
self, llm_context: llm.LLMContext, exposed_entities: dict | None
|
||||
|
@ -21,6 +21,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import llm
|
||||
|
||||
from .const import STATELESS_LLM_API
|
||||
from .llm_api import StatelessAssistAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -50,13 +53,21 @@ async def create_server(
|
||||
|
||||
server = Server("home-assistant")
|
||||
|
||||
async def get_api_instance() -> llm.APIInstance:
|
||||
"""Substitute the StatelessAssistAPI for the Assist API if selected."""
|
||||
if llm_api_id in (STATELESS_LLM_API, llm.LLM_API_ASSIST):
|
||||
api = StatelessAssistAPI(hass)
|
||||
return await api.async_get_api_instance(llm_context)
|
||||
|
||||
return await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
|
||||
@server.list_prompts() # type: ignore[no-untyped-call, misc]
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
llm_api = await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
llm_api = await get_api_instance()
|
||||
return [
|
||||
types.Prompt(
|
||||
name=llm_api.api.name,
|
||||
description=f"Default prompt for the Home Assistant LLM API {llm_api.api.name}",
|
||||
description=f"Default prompt for Home Assistant {llm_api.api.name} API",
|
||||
)
|
||||
]
|
||||
|
||||
@ -64,12 +75,12 @@ async def create_server(
|
||||
async def handle_get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
llm_api = await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
llm_api = await get_api_instance()
|
||||
if name != llm_api.api.name:
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
return types.GetPromptResult(
|
||||
description=f"Default prompt for the Home Assistant LLM API {llm_api.api.name}",
|
||||
description=f"Default prompt for Home Assistant {llm_api.api.name} API",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="assistant",
|
||||
@ -84,13 +95,13 @@ async def create_server(
|
||||
@server.list_tools() # type: ignore[no-untyped-call, misc]
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
"""List available time tools."""
|
||||
llm_api = await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
llm_api = await get_api_instance()
|
||||
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]
|
||||
|
||||
@server.call_tool() # type: ignore[no-untyped-call, misc]
|
||||
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
|
||||
"""Handle calling tools."""
|
||||
llm_api = await llm.async_get_api(hass, llm_api_id, llm_context)
|
||||
llm_api = await get_api_instance()
|
||||
tool_input = llm.ToolInput(tool_name=name, tool_args=arguments)
|
||||
_LOGGER.debug("Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args)
|
||||
|
||||
|
@ -146,11 +146,11 @@
|
||||
"services": {
|
||||
"get_mealplan": {
|
||||
"name": "Get mealplan",
|
||||
"description": "Get mealplan from Mealie",
|
||||
"description": "Gets a mealplan from Mealie",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "Mealie instance",
|
||||
"description": "Select the Mealie instance to get mealplan from"
|
||||
"description": "The Mealie instance to use for this action."
|
||||
},
|
||||
"start_date": {
|
||||
"name": "Start date",
|
||||
@ -164,7 +164,7 @@
|
||||
},
|
||||
"get_recipe": {
|
||||
"name": "Get recipe",
|
||||
"description": "Get recipe from Mealie",
|
||||
"description": "Gets a recipe from Mealie",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]",
|
||||
@ -178,7 +178,7 @@
|
||||
},
|
||||
"import_recipe": {
|
||||
"name": "Import recipe",
|
||||
"description": "Import recipe from an URL",
|
||||
"description": "Imports a recipe from an URL",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]",
|
||||
@ -196,7 +196,7 @@
|
||||
},
|
||||
"set_random_mealplan": {
|
||||
"name": "Set random mealplan",
|
||||
"description": "Set a random mealplan for a specific date",
|
||||
"description": "Sets a random mealplan for a specific date",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]",
|
||||
@ -214,7 +214,7 @@
|
||||
},
|
||||
"set_mealplan": {
|
||||
"name": "Set a mealplan",
|
||||
"description": "Set a mealplan for a specific date",
|
||||
"description": "Sets a mealplan for a specific date",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]",
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["moehlenhoff-alpha2==1.3.1"]
|
||||
"requirements": ["moehlenhoff-alpha2==1.4.0"]
|
||||
}
|
||||
|
@ -150,6 +150,7 @@ ABBREVIATIONS = {
|
||||
"pl_rst_pct": "payload_reset_percentage",
|
||||
"pl_rst_pr_mode": "payload_reset_preset_mode",
|
||||
"pl_stop": "payload_stop",
|
||||
"pl_stop_tilt": "payload_stop_tilt",
|
||||
"pl_strt": "payload_start",
|
||||
"pl_ret": "payload_return_to_base",
|
||||
"pl_toff": "payload_turn_off",
|
||||
|
@ -1022,8 +1022,6 @@ class MQTT:
|
||||
Resubscribe to all topics we were subscribed to and publish birth
|
||||
message.
|
||||
"""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
|
||||
if reason_code.is_failure:
|
||||
# 24: Continue authentication
|
||||
# 25: Re-authenticate
|
||||
|
@ -81,6 +81,7 @@ CONF_TILT_STATUS_TOPIC = "tilt_status_topic"
|
||||
CONF_TILT_STATUS_TEMPLATE = "tilt_status_template"
|
||||
|
||||
CONF_STATE_STOPPED = "state_stopped"
|
||||
CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt"
|
||||
CONF_TILT_CLOSED_POSITION = "tilt_closed_value"
|
||||
CONF_TILT_MAX = "tilt_max"
|
||||
CONF_TILT_MIN = "tilt_min"
|
||||
@ -203,6 +204,9 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PAYLOAD_STOP_TILT, default=DEFAULT_PAYLOAD_STOP): vol.Any(
|
||||
cv.string, None
|
||||
),
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@ -592,6 +596,12 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
self._attr_current_cover_tilt_position = tilt_percentage
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Stop moving the cover tilt."""
|
||||
await self.async_publish_with_config(
|
||||
self._config[CONF_TILT_COMMAND_TOPIC], self._config[CONF_PAYLOAD_STOP_TILT]
|
||||
)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
position_percentage = kwargs[ATTR_POSITION]
|
||||
|
@ -362,7 +362,7 @@
|
||||
"fields": {
|
||||
"evaluate_payload": {
|
||||
"name": "Evaluate payload",
|
||||
"description": "When `payload` is a Python bytes literal, evaluate the bytes literal and publish the raw data."
|
||||
"description": "If 'Payload' is a Python bytes literal, evaluate the bytes literal and publish the raw data."
|
||||
},
|
||||
"topic": {
|
||||
"name": "Topic",
|
||||
|
@ -101,6 +101,17 @@
|
||||
"medium": "Medium",
|
||||
"high": "High",
|
||||
"very_high": "Very high"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"very_low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_low%]",
|
||||
"low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::low%]",
|
||||
"medium": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::medium%]",
|
||||
"high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::high%]",
|
||||
"very_high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pmsx003_pm1": {
|
||||
@ -123,6 +134,17 @@
|
||||
"medium": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::medium%]",
|
||||
"high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::high%]",
|
||||
"very_high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_high%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"very_low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_low%]",
|
||||
"low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::low%]",
|
||||
"medium": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::medium%]",
|
||||
"high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::high%]",
|
||||
"very_high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sds011_pm10": {
|
||||
@ -148,6 +170,17 @@
|
||||
"medium": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::medium%]",
|
||||
"high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::high%]",
|
||||
"very_high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_high%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"very_low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_low%]",
|
||||
"low": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::low%]",
|
||||
"medium": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::medium%]",
|
||||
"high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::high%]",
|
||||
"very_high": "[%key:component::nam::entity::sensor::pmsx003_caqi_level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sps30_pm1": {
|
||||
|
@ -88,7 +88,7 @@
|
||||
"name": "Cache start time"
|
||||
},
|
||||
"nextcloud_cache_ttl": {
|
||||
"name": "Cache ttl"
|
||||
"name": "Cache TTL"
|
||||
},
|
||||
"nextcloud_database_size": {
|
||||
"name": "Database size"
|
||||
@ -268,13 +268,13 @@
|
||||
"name": "Updates available"
|
||||
},
|
||||
"nextcloud_system_cpuload_1": {
|
||||
"name": "CPU Load last 1 minute"
|
||||
"name": "CPU load last 1 minute"
|
||||
},
|
||||
"nextcloud_system_cpuload_15": {
|
||||
"name": "CPU Load last 15 minutes"
|
||||
"name": "CPU load last 15 minutes"
|
||||
},
|
||||
"nextcloud_system_cpuload_5": {
|
||||
"name": "CPU Load last 5 minutes"
|
||||
"name": "CPU load last 5 minutes"
|
||||
},
|
||||
"nextcloud_system_freespace": {
|
||||
"name": "Free space"
|
||||
|
@ -36,6 +36,7 @@ from .const import (
|
||||
ATTR_SETTINGS,
|
||||
ATTR_STATUS,
|
||||
CONF_PROFILE_ID,
|
||||
DOMAIN,
|
||||
UPDATE_INTERVAL_ANALYTICS,
|
||||
UPDATE_INTERVAL_CONNECTION,
|
||||
UPDATE_INTERVAL_SETTINGS,
|
||||
@ -88,9 +89,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b
|
||||
try:
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"entry": entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
except InvalidApiKeyError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
translation_placeholders={"entry": entry.title},
|
||||
) from err
|
||||
|
||||
tasks = []
|
||||
coordinators = {}
|
||||
|
@ -2,15 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nextdns import AnalyticsStatus
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import AnalyticsStatus, ApiError, InvalidApiKeyError
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@ -53,4 +57,21 @@ class NextDnsButton(
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger cleaning logs."""
|
||||
await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id)
|
||||
try:
|
||||
await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id)
|
||||
except (
|
||||
ApiError,
|
||||
ClientConnectorError,
|
||||
TimeoutError,
|
||||
ClientError,
|
||||
) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="method_error",
|
||||
translation_placeholders={
|
||||
"entity": self.entity_id,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
except InvalidApiKeyError:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
|
@ -79,9 +79,20 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
ClientConnectorError,
|
||||
RetryError,
|
||||
) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
"error": repr(err),
|
||||
},
|
||||
) from err
|
||||
except InvalidApiKeyError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
translation_placeholders={"entry": self.config_entry.title},
|
||||
) from err
|
||||
|
||||
async def _async_update_data_internal(self) -> CoordinatorDataT:
|
||||
"""Update data via library."""
|
||||
|
@ -359,5 +359,19 @@
|
||||
"name": "Force YouTube restricted mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication failed for {entry}, please update your API key"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while connecting to the NextDNS API for {entry}: {error}"
|
||||
},
|
||||
"method_error": {
|
||||
"message": "An error occurred while calling the NextDNS API method for {entity}: {error}"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the NextDNS API for {entry}: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user