Merge branch 'dev' into freeze_timeout_installing_packages

This commit is contained in:
J. Nick Koston 2024-08-26 12:35:07 -10:00 committed by GitHub
commit 6ec9e9250e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4484 changed files with 195962 additions and 127453 deletions

View File

@ -49,6 +49,7 @@ base_platforms: &base_platforms
- homeassistant/components/tts/** - homeassistant/components/tts/**
- homeassistant/components/update/** - homeassistant/components/update/**
- homeassistant/components/vacuum/** - homeassistant/components/vacuum/**
- homeassistant/components/valve/**
- homeassistant/components/water_heater/** - homeassistant/components/water_heater/**
- homeassistant/components/weather/** - homeassistant/components/weather/**
@ -145,6 +146,7 @@ requirements: &requirements
- homeassistant/package_constraints.txt - homeassistant/package_constraints.txt
- requirements*.txt - requirements*.txt
- pyproject.toml - pyproject.toml
- script/licenses.py
any: any:
- *base_platforms - *base_platforms

File diff suppressed because it is too large Load Diff

View File

@ -74,7 +74,6 @@ If the code communicates with devices, web services, or third-party tools:
- [ ] New or updated dependencies have been added to `requirements_all.txt`. - [ ] New or updated dependencies have been added to `requirements_all.txt`.
Updated by running `python3 -m script.gen_requirements_all`. Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description. - [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
- [ ] Untested files have been added to `.coveragerc`.
<!-- <!--
This project is very active and we have a high turnover of pull requests. This project is very active and we have a high turnover of pull requests.

View File

@ -32,7 +32,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz
@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: translations name: translations
@ -190,14 +190,14 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.3.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2024.03.5 uses: home-assistant/builder@2024.08.2
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -256,14 +256,14 @@ jobs:
fi fi
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.3.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2024.03.5 uses: home-assistant/builder@2024.08.2
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -323,20 +323,20 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0 uses: sigstore/cosign-installer@v3.6.0
with: with:
cosign-release: "v2.2.3" cosign-release: "v2.2.3"
- name: Login to DockerHub - name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant' if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.3.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant' if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.3.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -453,12 +453,12 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: translations name: translations

View File

@ -31,12 +31,16 @@ on:
description: "Only run mypy" description: "Only run mypy"
default: false default: false
type: boolean type: boolean
audit-licenses-only:
description: "Only run audit licenses"
default: false
type: boolean
env: env:
CACHE_VERSION: 9 CACHE_VERSION: 10
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.8" HA_SHORT_VERSION: "2024.9"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -86,7 +90,7 @@ jobs:
tests_glob: ${{ steps.info.outputs.tests_glob }} tests_glob: ${{ steps.info.outputs.tests_glob }}
tests: ${{ steps.info.outputs.tests }} tests: ${{ steps.info.outputs.tests }}
skip_coverage: ${{ steps.info.outputs.skip_coverage }} skip_coverage: ${{ steps.info.outputs.skip_coverage }}
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
@ -218,10 +222,11 @@ jobs:
pre-commit: pre-commit:
name: Prepare pre-commit base name: Prepare pre-commit base
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
github.event.inputs.pylint-only != 'true' github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs: needs:
- info - info
steps: steps:
@ -229,7 +234,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -266,7 +271,7 @@ jobs:
lint-ruff-format: lint-ruff-format:
name: Check ruff-format name: Check ruff-format
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- info - info
- pre-commit - pre-commit
@ -274,7 +279,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -306,7 +311,7 @@ jobs:
lint-ruff: lint-ruff:
name: Check ruff name: Check ruff
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- info - info
- pre-commit - pre-commit
@ -314,7 +319,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -343,9 +348,10 @@ jobs:
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
env: env:
RUFF_OUTPUT_FORMAT: github RUFF_OUTPUT_FORMAT: github
lint-other: lint-other:
name: Check other linters name: Check other linters
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- info - info
- pre-commit - pre-commit
@ -353,7 +359,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -437,7 +443,7 @@ jobs:
base: base:
name: Prepare dependencies name: Prepare dependencies
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: info needs: info
timeout-minutes: 60 timeout-minutes: 60
strategy: strategy:
@ -448,7 +454,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -508,16 +514,16 @@ jobs:
uv pip install -U "pip>=21.3.1" setuptools wheel uv pip install -U "pip>=21.3.1" setuptools wheel
uv pip install -r requirements.txt uv pip install -r requirements.txt
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
uv pip install -r requirements_all_pytest.txt uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
uv pip install -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat uv pip install -e . --config-settings editable_mode=compat
hassfest: hassfest:
name: Check hassfest name: Check hassfest
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
github.event.inputs.pylint-only != 'true' github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs: needs:
- info - info
- base - base
@ -532,7 +538,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -552,10 +558,11 @@ jobs:
gen-requirements-all: gen-requirements-all:
name: Check all requirements name: Check all requirements
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
github.event.inputs.pylint-only != 'true' github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs: needs:
- info - info
- base - base
@ -564,7 +571,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -582,12 +589,56 @@ jobs:
. venv/bin/activate . venv/bin/activate
python -m script.gen_requirements_all validate python -m script.gen_requirements_all validate
audit-licenses:
name: Audit licenses
runs-on: ubuntu-24.04
needs:
- info
- base
if: |
(github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
|| github.event.inputs.audit-licenses-only == 'true')
&& needs.info.outputs.requirements == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.2
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run pip-licenses
run: |
. venv/bin/activate
pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses
uses: actions/upload-artifact@v4.3.6
with:
name: licenses
path: licenses.json
- name: Process licenses
run: |
. venv/bin/activate
python -m script.licenses
pylint: pylint:
name: Check pylint name: Check pylint
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
timeout-minutes: 20 timeout-minutes: 20
if: | if: |
github.event.inputs.mypy-only != 'true' github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.pylint-only == 'true' || github.event.inputs.pylint-only == 'true'
needs: needs:
- info - info
@ -597,7 +648,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -629,10 +680,12 @@ jobs:
pylint-tests: pylint-tests:
name: Check pylint on tests name: Check pylint on tests
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
timeout-minutes: 20 timeout-minutes: 20
if: | if: |
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true') (github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.pylint-only == 'true')
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true') && (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
needs: needs:
- info - info
@ -642,7 +695,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -663,20 +716,21 @@ jobs:
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y tests pylint tests
- name: Run pylint (partially) - name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
shell: bash shell: bash
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y tests/components/${{ needs.info.outputs.tests_glob }} pylint tests/components/${{ needs.info.outputs.tests_glob }}
mypy: mypy:
name: Check mypy name: Check mypy
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
github.event.inputs.pylint-only != 'true' github.event.inputs.pylint-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.mypy-only == 'true' || github.event.inputs.mypy-only == 'true'
needs: needs:
- info - info
@ -686,7 +740,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -735,12 +789,13 @@ jobs:
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
prepare-pytest-full: prepare-pytest-full:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.test_full_suite == 'true' && needs.info.outputs.test_full_suite == 'true'
needs: needs:
- info - info
@ -760,7 +815,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -778,19 +833,20 @@ jobs:
. venv/bin/activate . venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets - name: Upload pytest_buckets
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest_buckets name: pytest_buckets
path: pytest_buckets.txt path: pytest_buckets.txt
overwrite: true overwrite: true
pytest-full: pytest-full:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.test_full_suite == 'true' && needs.info.outputs.test_full_suite == 'true'
needs: needs:
- info - info
@ -823,7 +879,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -842,7 +898,7 @@ jobs:
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets - name: Download pytest_buckets
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: pytest_buckets name: pytest_buckets
- name: Compile English translations - name: Compile English translations
@ -864,6 +920,7 @@ jobs:
cov_params+=(--cov-report=xml) cov_params+=(--cov-report=xml)
fi fi
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
python3 -b -X dev -m pytest \ python3 -b -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
@ -877,14 +934,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure' if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@ -896,7 +953,7 @@ jobs:
./script/check_dirty ./script/check_dirty
pytest-mariadb: pytest-mariadb:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
services: services:
mariadb: mariadb:
image: ${{ matrix.mariadb-group }} image: ${{ matrix.mariadb-group }}
@ -910,6 +967,7 @@ jobs:
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.mariadb_groups != '[]' && needs.info.outputs.mariadb_groups != '[]'
needs: needs:
- info - info
@ -941,7 +999,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1002,7 +1060,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@ -1010,7 +1068,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@ -1035,6 +1093,7 @@ jobs:
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.postgresql_groups != '[]' && needs.info.outputs.postgresql_groups != '[]'
needs: needs:
- info - info
@ -1066,7 +1125,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1128,7 +1187,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@ -1136,7 +1195,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@ -1149,7 +1208,7 @@ jobs:
coverage-full: coverage-full:
name: Upload test coverage to Codecov (full suite) name: Upload test coverage to Codecov (full suite)
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- info - info
- pytest-full - pytest-full
@ -1160,7 +1219,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
@ -1173,12 +1232,13 @@ jobs:
version: v0.6.0 version: v0.6.0
pytest-partial: pytest-partial:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
if: | if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.tests_glob && needs.info.outputs.tests_glob
&& needs.info.outputs.test_full_suite == 'false' && needs.info.outputs.test_full_suite == 'false'
needs: needs:
@ -1211,7 +1271,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1269,14 +1329,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@ -1288,7 +1348,7 @@ jobs:
coverage-partial: coverage-partial:
name: Upload test coverage to Codecov (partial suite) name: Upload test coverage to Codecov (partial suite)
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- info - info
- pytest-partial - pytest-partial
@ -1297,7 +1357,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov

View File

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

View File

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

View File

@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -82,14 +82,14 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
overwrite: true overwrite: true
- name: Upload requirements_diff - name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
@ -101,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels - name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.6
with: with:
name: requirements_all_wheels name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt path: ./requirements_all_wheels_*.txt
@ -121,17 +121,17 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: requirements_diff name: requirements_diff
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -159,17 +159,17 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: requirements_diff name: requirements_diff
- name: Download requirements_all_wheels - name: Download requirements_all_wheels
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.8
with: with:
name: requirements_all_wheels name: requirements_all_wheels
@ -203,7 +203,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (old cython) - name: Build wheels (old cython)
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -211,14 +211,14 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt" requirements: "requirements_old-cython.txt"
pip: "'cython<3'" pip: "'cython<3'"
- name: Build wheels (part 1) - name: Build wheels (part 1)
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -226,13 +226,13 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
- name: Build wheels (part 2) - name: Build wheels (part 2)
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -240,13 +240,13 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
- name: Build wheels (part 3) - name: Build wheels (part 3)
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -254,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac" requirements: "requirements_all.txtac"

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.9 rev: v0.6.2
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -12,7 +12,7 @@ repos:
hooks: hooks:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue - --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
- --skip="./.*,*.csv,*.json,*.ambr" - --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json, html] exclude_types: [csv, json, html]
@ -83,7 +83,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$ files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
- id: hassfest-metadata - id: hassfest-metadata
name: hassfest-metadata name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata

View File

@ -21,6 +21,7 @@ homeassistant.helpers.entity_platform
homeassistant.helpers.entity_values homeassistant.helpers.entity_values
homeassistant.helpers.event homeassistant.helpers.event
homeassistant.helpers.reload homeassistant.helpers.reload
homeassistant.helpers.script
homeassistant.helpers.script_variables homeassistant.helpers.script_variables
homeassistant.helpers.singleton homeassistant.helpers.singleton
homeassistant.helpers.sun homeassistant.helpers.sun
@ -94,9 +95,8 @@ homeassistant.components.aruba.*
homeassistant.components.arwn.* homeassistant.components.arwn.*
homeassistant.components.aseko_pool_live.* homeassistant.components.aseko_pool_live.*
homeassistant.components.assist_pipeline.* homeassistant.components.assist_pipeline.*
homeassistant.components.asterisk_cdr.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.* homeassistant.components.asuswrt.*
homeassistant.components.autarco.*
homeassistant.components.auth.* homeassistant.components.auth.*
homeassistant.components.automation.* homeassistant.components.automation.*
homeassistant.components.awair.* homeassistant.components.awair.*
@ -118,6 +118,7 @@ homeassistant.components.bond.*
homeassistant.components.braviatv.* homeassistant.components.braviatv.*
homeassistant.components.brother.* homeassistant.components.brother.*
homeassistant.components.browser.* homeassistant.components.browser.*
homeassistant.components.bryant_evolution.*
homeassistant.components.bthome.* homeassistant.components.bthome.*
homeassistant.components.button.* homeassistant.components.button.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
@ -165,6 +166,7 @@ homeassistant.components.ecowitt.*
homeassistant.components.efergy.* homeassistant.components.efergy.*
homeassistant.components.electrasmart.* homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.* homeassistant.components.electric_kiwi.*
homeassistant.components.elevenlabs.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.elkm1.* homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.* homeassistant.components.emulated_hue.*
@ -194,7 +196,9 @@ homeassistant.components.fritzbox.*
homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fritzbox_callmonitor.*
homeassistant.components.fronius.* homeassistant.components.fronius.*
homeassistant.components.frontend.* homeassistant.components.frontend.*
homeassistant.components.fujitsu_fglair.*
homeassistant.components.fully_kiosk.* homeassistant.components.fully_kiosk.*
homeassistant.components.fyta.*
homeassistant.components.generic_hygrostat.* homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.* homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
@ -253,6 +257,7 @@ homeassistant.components.integration.*
homeassistant.components.intent.* homeassistant.components.intent.*
homeassistant.components.intent_script.* homeassistant.components.intent_script.*
homeassistant.components.ios.* homeassistant.components.ios.*
homeassistant.components.iotty.*
homeassistant.components.ipp.* homeassistant.components.ipp.*
homeassistant.components.iqvia.* homeassistant.components.iqvia.*
homeassistant.components.islamic_prayer_times.* homeassistant.components.islamic_prayer_times.*
@ -277,6 +282,7 @@ homeassistant.components.lidarr.*
homeassistant.components.lifx.* homeassistant.components.lifx.*
homeassistant.components.light.* homeassistant.components.light.*
homeassistant.components.linear_garage_door.* homeassistant.components.linear_garage_door.*
homeassistant.components.linkplay.*
homeassistant.components.litejet.* homeassistant.components.litejet.*
homeassistant.components.litterrobot.* homeassistant.components.litterrobot.*
homeassistant.components.local_ip.* homeassistant.components.local_ip.*
@ -287,7 +293,9 @@ homeassistant.components.logger.*
homeassistant.components.london_underground.* homeassistant.components.london_underground.*
homeassistant.components.lookin.* homeassistant.components.lookin.*
homeassistant.components.luftdaten.* homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.manual.*
homeassistant.components.map.* homeassistant.components.map.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
homeassistant.components.matrix.* homeassistant.components.matrix.*
@ -382,6 +390,7 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.schedule.* homeassistant.components.schedule.*
homeassistant.components.scrape.* homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.* homeassistant.components.search.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensibo.* homeassistant.components.sensibo.*

42
.vscode/launch.json vendored
View File

@ -6,38 +6,52 @@
"configurations": [ "configurations": [
{ {
"name": "Home Assistant", "name": "Home Assistant",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "homeassistant", "module": "homeassistant",
"justMyCode": false, "justMyCode": false,
"args": ["--debug", "-c", "config"], "args": [
"--debug",
"-c",
"config"
],
"preLaunchTask": "Compile English translations" "preLaunchTask": "Compile English translations"
}, },
{ {
"name": "Home Assistant (skip pip)", "name": "Home Assistant (skip pip)",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "homeassistant", "module": "homeassistant",
"justMyCode": false, "justMyCode": false,
"args": ["--debug", "-c", "config", "--skip-pip"], "args": [
"--debug",
"-c",
"config",
"--skip-pip"
],
"preLaunchTask": "Compile English translations" "preLaunchTask": "Compile English translations"
}, },
{ {
"name": "Home Assistant: Changed tests", "name": "Home Assistant: Changed tests",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "pytest", "module": "pytest",
"justMyCode": false, "justMyCode": false,
"args": ["--timeout=10", "--picked"], "args": [
"--timeout=10",
"--picked"
],
}, },
{ {
// Debug by attaching to local Home Assistant server using Remote Python Debugger. // Debug by attaching to local Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/ // See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Local", "name": "Home Assistant: Attach Local",
"type": "python", "type": "debugpy",
"request": "attach", "request": "attach",
"port": 5678, "connect": {
"host": "localhost", "port": 5678,
"host": "localhost"
},
"pathMappings": [ "pathMappings": [
{ {
"localRoot": "${workspaceFolder}", "localRoot": "${workspaceFolder}",
@ -49,10 +63,12 @@
// Debug by attaching to remote Home Assistant server using Remote Python Debugger. // Debug by attaching to remote Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/ // See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Remote", "name": "Home Assistant: Attach Remote",
"type": "python", "type": "debugpy",
"request": "attach", "request": "attach",
"port": 5678, "connect": {
"host": "homeassistant.local", "port": 5678,
"host": "homeassistant.local"
},
"pathMappings": [ "pathMappings": [
{ {
"localRoot": "${workspaceFolder}", "localRoot": "${workspaceFolder}",
@ -61,4 +77,4 @@
] ]
} }
] ]
} }

1
.vscode/tasks.json vendored
View File

@ -76,6 +76,7 @@
"detail": "Generate code coverage report for a given integration.", "detail": "Generate code coverage report for a given integration.",
"type": "shell", "type": "shell",
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto", "command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
"dependsOn": ["Compile English translations"],
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true

View File

@ -80,8 +80,6 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari /tests/components/airzone/ @Noltari
/homeassistant/components/airzone_cloud/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari
/tests/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari
/homeassistant/components/aladdin_connect/ @swcloudgenie
/tests/components/aladdin_connect/ @swcloudgenie
/homeassistant/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alarm_control_panel/ @home-assistant/core
/tests/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck /homeassistant/components/alert/ @home-assistant/core @frenck
@ -110,6 +108,8 @@ build.json @home-assistant/supervisor
/tests/components/anova/ @Lash-L /tests/components/anova/ @Lash-L
/homeassistant/components/anthemav/ @hyralex /homeassistant/components/anthemav/ @hyralex
/tests/components/anthemav/ @hyralex /tests/components/anthemav/ @hyralex
/homeassistant/components/anthropic/ @Shulyaka
/tests/components/anthropic/ @Shulyaka
/homeassistant/components/aosmith/ @bdr99 /homeassistant/components/aosmith/ @bdr99
/tests/components/aosmith/ @bdr99 /tests/components/aosmith/ @bdr99
/homeassistant/components/apache_kafka/ @bachya /homeassistant/components/apache_kafka/ @bachya
@ -157,6 +157,8 @@ build.json @home-assistant/supervisor
/tests/components/aurora_abb_powerone/ @davet2001 /tests/components/aurora_abb_powerone/ @davet2001
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77 /homeassistant/components/aussie_broadband/ @nickw444 @Bre77
/tests/components/aussie_broadband/ @nickw444 @Bre77 /tests/components/aussie_broadband/ @nickw444 @Bre77
/homeassistant/components/autarco/ @klaasnicolaas
/tests/components/autarco/ @klaasnicolaas
/homeassistant/components/auth/ @home-assistant/core /homeassistant/components/auth/ @home-assistant/core
/tests/components/auth/ @home-assistant/core /tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core /homeassistant/components/automation/ @home-assistant/core
@ -197,7 +199,8 @@ build.json @home-assistant/supervisor
/tests/components/bluemaestro/ @bdraco /tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core /homeassistant/components/blueprint/ @home-assistant/core
/tests/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core
/homeassistant/components/bluesound/ @thrawnarn /homeassistant/components/bluesound/ @thrawnarn @LouisChrist
/tests/components/bluesound/ @thrawnarn @LouisChrist
/homeassistant/components/bluetooth/ @bdraco /homeassistant/components/bluetooth/ @bdraco
/tests/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco
/homeassistant/components/bluetooth_adapters/ @bdraco /homeassistant/components/bluetooth_adapters/ @bdraco
@ -220,6 +223,8 @@ build.json @home-assistant/supervisor
/tests/components/brottsplatskartan/ @gjohansson-ST /tests/components/brottsplatskartan/ @gjohansson-ST
/homeassistant/components/brunt/ @eavanvalkenburg /homeassistant/components/brunt/ @eavanvalkenburg
/tests/components/brunt/ @eavanvalkenburg /tests/components/brunt/ @eavanvalkenburg
/homeassistant/components/bryant_evolution/ @danielsmyers
/tests/components/bryant_evolution/ @danielsmyers
/homeassistant/components/bsblan/ @liudger /homeassistant/components/bsblan/ @liudger
/tests/components/bsblan/ @liudger /tests/components/bsblan/ @liudger
/homeassistant/components/bt_smarthub/ @typhoon2099 /homeassistant/components/bt_smarthub/ @typhoon2099
@ -239,6 +244,8 @@ build.json @home-assistant/supervisor
/tests/components/ccm15/ @ocalvo /tests/components/ccm15/ @ocalvo
/homeassistant/components/cert_expiry/ @jjlawren /homeassistant/components/cert_expiry/ @jjlawren
/tests/components/cert_expiry/ @jjlawren /tests/components/cert_expiry/ @jjlawren
/homeassistant/components/chacon_dio/ @cnico
/tests/components/chacon_dio/ @cnico
/homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl
@ -342,8 +349,8 @@ build.json @home-assistant/supervisor
/tests/components/dremel_3d_printer/ @tkdrob /tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck /homeassistant/components/dsmr/ @Robbie1221
/tests/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
@ -360,8 +367,8 @@ build.json @home-assistant/supervisor
/tests/components/ecoforest/ @pjanuario /tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @w1ll1am23 /homeassistant/components/econet/ @w1ll1am23
/tests/components/econet/ @w1ll1am23 /tests/components/econet/ @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar /homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar /tests/components/ecovacs/ @mib1185 @edenhaus @Augar
/homeassistant/components/ecowitt/ @pvizeli /homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli /tests/components/ecowitt/ @pvizeli
/homeassistant/components/efergy/ @tkdrob /homeassistant/components/efergy/ @tkdrob
@ -371,6 +378,8 @@ build.json @home-assistant/supervisor
/tests/components/electrasmart/ @jafar-atili /tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000 /homeassistant/components/electric_kiwi/ @mikey0000
/tests/components/electric_kiwi/ @mikey0000 /tests/components/electric_kiwi/ @mikey0000
/homeassistant/components/elevenlabs/ @sorgfresser
/tests/components/elevenlabs/ @sorgfresser
/homeassistant/components/elgato/ @frenck /homeassistant/components/elgato/ @frenck
/tests/components/elgato/ @frenck /tests/components/elgato/ @frenck
/homeassistant/components/elkm1/ @gwww @bdraco /homeassistant/components/elkm1/ @gwww @bdraco
@ -382,6 +391,7 @@ build.json @home-assistant/supervisor
/tests/components/elvia/ @ludeeus /tests/components/elvia/ @ludeeus
/homeassistant/components/emby/ @mezz64 /homeassistant/components/emby/ @mezz64
/homeassistant/components/emoncms/ @borpin @alexandrecuer /homeassistant/components/emoncms/ @borpin @alexandrecuer
/tests/components/emoncms/ @borpin @alexandrecuer
/homeassistant/components/emonitor/ @bdraco /homeassistant/components/emonitor/ @bdraco
/tests/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco
/homeassistant/components/emulated_hue/ @bdraco @Tho85 /homeassistant/components/emulated_hue/ @bdraco @Tho85
@ -398,8 +408,8 @@ build.json @home-assistant/supervisor
/tests/components/enigma2/ @autinerd /tests/components/enigma2/ @autinerd
/homeassistant/components/enocean/ @bdurrer /homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac /tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
@ -423,6 +433,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/evil_genius_labs/ @balloob /homeassistant/components/evil_genius_labs/ @balloob
/tests/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb /homeassistant/components/evohome/ @zxdavb
/tests/components/evohome/ @zxdavb
/homeassistant/components/ezviz/ @RenierM26 @baqs /homeassistant/components/ezviz/ @RenierM26 @baqs
/tests/components/ezviz/ @RenierM26 @baqs /tests/components/ezviz/ @RenierM26 @baqs
/homeassistant/components/faa_delays/ @ntilley905 /homeassistant/components/faa_delays/ @ntilley905
@ -431,6 +442,8 @@ build.json @home-assistant/supervisor
/tests/components/fan/ @home-assistant/core /tests/components/fan/ @home-assistant/core
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna /homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna /tests/components/fastdotcom/ @rohankapoorcom @erwindouna
/homeassistant/components/feedreader/ @mib1185
/tests/components/feedreader/ @mib1185
/homeassistant/components/fibaro/ @rappenze /homeassistant/components/fibaro/ @rappenze
/tests/components/fibaro/ @rappenze /tests/components/fibaro/ @rappenze
/homeassistant/components/file/ @fabaff /homeassistant/components/file/ @fabaff
@ -486,6 +499,8 @@ build.json @home-assistant/supervisor
/tests/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend
/homeassistant/components/frontier_silicon/ @wlcrs /homeassistant/components/frontier_silicon/ @wlcrs
/tests/components/frontier_silicon/ @wlcrs /tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fujitsu_fglair/ @crevetor
/tests/components/fujitsu_fglair/ @crevetor
/homeassistant/components/fully_kiosk/ @cgarwood /homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli /homeassistant/components/fyta/ @dontinelli
@ -501,6 +516,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/generic_hygrostat/ @Shulyaka /homeassistant/components/generic_hygrostat/ @Shulyaka
/tests/components/generic_hygrostat/ @Shulyaka /tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti /homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/geo_json_events/ @exxamalte /homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte /tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core /homeassistant/components/geo_location/ @home-assistant/core
@ -691,6 +707,8 @@ build.json @home-assistant/supervisor
/tests/components/ios/ @robbiet480 /tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard /tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iotty/ @pburgio
/tests/components/iotty/ @pburgio
/homeassistant/components/iperf3/ @rohankapoorcom /homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes /homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes /tests/components/ipma/ @dgomes
@ -699,10 +717,14 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya /homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya /tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50 /homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/iron_os/ @tr4nt0r
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco /homeassistant/components/isal/ @bdraco
/tests/components/isal/ @bdraco /tests/components/isal/ @bdraco
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair /homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair /tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/israel_rail/ @shaiu
/tests/components/israel_rail/ @shaiu
/homeassistant/components/iss/ @DurgNomis-drol /homeassistant/components/iss/ @DurgNomis-drol
/tests/components/iss/ @DurgNomis-drol /tests/components/iss/ @DurgNomis-drol
/homeassistant/components/ista_ecotrend/ @tr4nt0r /homeassistant/components/ista_ecotrend/ @tr4nt0r
@ -737,8 +759,8 @@ build.json @home-assistant/supervisor
/tests/components/kitchen_sink/ @home-assistant/core /tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes /homeassistant/components/kmtronic/ @dgomes
/tests/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes
/homeassistant/components/knocki/ @joostlek @jgatto1 /homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
/tests/components/knocki/ @joostlek @jgatto1 /tests/components/knocki/ @joostlek @jgatto1 @JakeBosh
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
/tests/components/knx/ @Julius2342 @farmio @marvin-w /tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund /homeassistant/components/kodi/ @OnFreund
@ -779,10 +801,14 @@ build.json @home-assistant/supervisor
/tests/components/lg_netcast/ @Drafteed @splinter98 /tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core /homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT /homeassistant/components/linear_garage_door/ @IceBotYT
/tests/components/linear_garage_door/ @IceBotYT /tests/components/linear_garage_door/ @IceBotYT
/homeassistant/components/linkplay/ @Velleman
/tests/components/linkplay/ @Velleman
/homeassistant/components/linux_battery/ @fabaff /homeassistant/components/linux_battery/ @fabaff
/homeassistant/components/litejet/ @joncar /homeassistant/components/litejet/ @joncar
/tests/components/litejet/ @joncar /tests/components/litejet/ @joncar
@ -802,8 +828,6 @@ build.json @home-assistant/supervisor
/tests/components/logbook/ @home-assistant/core /tests/components/logbook/ @home-assistant/core
/homeassistant/components/logger/ @home-assistant/core /homeassistant/components/logger/ @home-assistant/core
/tests/components/logger/ @home-assistant/core /tests/components/logger/ @home-assistant/core
/homeassistant/components/logi_circle/ @evanjd
/tests/components/logi_circle/ @evanjd
/homeassistant/components/london_underground/ @jpbede /homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede /tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco /homeassistant/components/lookin/ @ANMalko @bdraco
@ -823,13 +847,16 @@ build.json @home-assistant/supervisor
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151 /tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001 /homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001
/homeassistant/components/mastodon/ @fabaff /homeassistant/components/madvr/ @iloveicedgreentea
/tests/components/madvr/ @iloveicedgreentea
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
/tests/components/mastodon/ @fabaff @andrew-codechimp
/homeassistant/components/matrix/ @PaarthShah /homeassistant/components/matrix/ @PaarthShah
/tests/components/matrix/ @PaarthShah /tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter /homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter
/homeassistant/components/mealie/ @joostlek /homeassistant/components/mealie/ @joostlek @andrew-codechimp
/tests/components/mealie/ @joostlek /tests/components/mealie/ @joostlek @andrew-codechimp
/homeassistant/components/meater/ @Sotolotl @emontnemery /homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue /homeassistant/components/medcom_ble/ @elafargue
@ -874,8 +901,6 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco /tests/components/moat/ @bdraco
/homeassistant/components/mobile_app/ @home-assistant/core /homeassistant/components/mobile_app/ @home-assistant/core
/tests/components/mobile_app/ @home-assistant/core /tests/components/mobile_app/ @home-assistant/core
/homeassistant/components/modbus/ @janiversen
/tests/components/modbus/ @janiversen
/homeassistant/components/modem_callerid/ @tkdrob /homeassistant/components/modem_callerid/ @tkdrob
/tests/components/modem_callerid/ @tkdrob /tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug /homeassistant/components/modern_forms/ @wonderslug
@ -945,6 +970,8 @@ build.json @home-assistant/supervisor
/tests/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob
/homeassistant/components/nibe_heatpump/ @elupus /homeassistant/components/nibe_heatpump/ @elupus
/tests/components/nibe_heatpump/ @elupus /tests/components/nibe_heatpump/ @elupus
/homeassistant/components/nice_go/ @IceBotYT
/tests/components/nice_go/ @IceBotYT
/homeassistant/components/nightscout/ @marciogranzotto /homeassistant/components/nightscout/ @marciogranzotto
/tests/components/nightscout/ @marciogranzotto /tests/components/nightscout/ @marciogranzotto
/homeassistant/components/nilu/ @hfurubotten /homeassistant/components/nilu/ @hfurubotten
@ -987,8 +1014,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/ollama/ @synesthesiam /homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam /tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont /homeassistant/components/ombi/ @larssont
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
/homeassistant/components/onboarding/ @home-assistant/core /homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core /tests/components/onboarding/ @home-assistant/core
/homeassistant/components/oncue/ @bdraco @peterager /homeassistant/components/oncue/ @bdraco @peterager
@ -1033,8 +1058,8 @@ build.json @home-assistant/supervisor
/tests/components/otbr/ @home-assistant/core /tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund /homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund /tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 /homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 /tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/homeassistant/components/ovo_energy/ @timmo001 /homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001 /tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas /homeassistant/components/p1_monitor/ @klaasnicolaas
@ -1204,6 +1229,8 @@ build.json @home-assistant/supervisor
/tests/components/rtsp_to_webrtc/ @allenporter /tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/russound_rio/ @noahhusby
/tests/components/russound_rio/ @noahhusby
/homeassistant/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvi_gateway/ @akx
/tests/components/ruuvi_gateway/ @akx /tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx /homeassistant/components/ruuvitag_ble/ @akx
@ -1271,6 +1298,8 @@ build.json @home-assistant/supervisor
/tests/components/sighthound/ @robmarkcole /tests/components/sighthound/ @robmarkcole
/homeassistant/components/signal_messenger/ @bbernhard /homeassistant/components/signal_messenger/ @bbernhard
/tests/components/signal_messenger/ @bbernhard /tests/components/signal_messenger/ @bbernhard
/homeassistant/components/simplefin/ @scottg489 @jeeftor
/tests/components/simplefin/ @scottg489 @jeeftor
/homeassistant/components/simplepush/ @engrbm87 /homeassistant/components/simplepush/ @engrbm87
/tests/components/simplepush/ @engrbm87 /tests/components/simplepush/ @engrbm87
/homeassistant/components/simplisafe/ @bachya /homeassistant/components/simplisafe/ @bachya
@ -1300,6 +1329,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/smarty/ @z0mbieprocess /homeassistant/components/smarty/ @z0mbieprocess
/homeassistant/components/smhi/ @gjohansson-ST /homeassistant/components/smhi/ @gjohansson-ST
/tests/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl
/tests/components/smlight/ @tl-sl
/homeassistant/components/sms/ @ocalvo /homeassistant/components/sms/ @ocalvo
/tests/components/sms/ @ocalvo /tests/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123 /homeassistant/components/snapcast/ @luar123
@ -1422,6 +1453,8 @@ build.json @home-assistant/supervisor
/tests/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core /homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core /tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks /homeassistant/components/tesla_wall_connector/ @einarhauks
/tests/components/tesla_wall_connector/ @einarhauks /tests/components/tesla_wall_connector/ @einarhauks
/homeassistant/components/teslemetry/ @Bre77 /homeassistant/components/teslemetry/ @Bre77

View File

@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU ARG QEMU_CPU
# Install uv # Install uv
RUN pip3 install uv==0.2.13 RUN pip3 install uv==0.2.27
WORKDIR /usr/src WORKDIR /usr/src

View File

@ -42,7 +42,8 @@ WORKDIR /usr/src
# Setup hass-release # Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
&& uv pip install --system -e hass-release/ && uv pip install --system -e hass-release/ \
&& chown -R vscode /usr/src/hass-release/data
USER vscode USER vscode
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv" ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"

View File

@ -4,7 +4,7 @@ coverage:
status: status:
project: project:
default: default:
target: 90 target: auto
threshold: 0.09 threshold: 0.09
required: required:
target: auto target: auto

View File

@ -363,15 +363,15 @@ class AuthManager:
local_only: bool | None = None, local_only: bool | None = None,
) -> None: ) -> None:
"""Update a user.""" """Update a user."""
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {
attr_name: value
for attr_name, value in ( for attr_name, value in (
("name", name), ("name", name),
("group_ids", group_ids), ("group_ids", group_ids),
("local_only", local_only), ("local_only", local_only),
): )
if value is not None: if value is not None
kwargs[attr_name] = value }
await self._store.async_update_user(user, **kwargs) await self._store.async_update_user(user, **kwargs)
if is_active is not None: if is_active is not None:

View File

@ -105,14 +105,18 @@ class AuthStore:
"perm_lookup": self._perm_lookup, "perm_lookup": self._perm_lookup,
} }
for attr_name, value in ( kwargs.update(
("is_owner", is_owner), {
("is_active", is_active), attr_name: value
("local_only", local_only), for attr_name, value in (
("system_generated", system_generated), ("is_owner", is_owner),
): ("is_active", is_active),
if value is not None: ("local_only", local_only),
kwargs[attr_name] = value ("system_generated", system_generated),
)
if value is not None
}
)
new_user = models.User(**kwargs) new_user = models.User(**kwargs)

View File

@ -18,9 +18,12 @@ from homeassistant.const import (
EVENT_THEMES_UPDATED, EVENT_THEMES_UPDATED,
) )
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED
from homeassistant.util.event_type import EventType from homeassistant.util.event_type import EventType
# These are events that do not contain any sensitive data # These are events that do not contain any sensitive data
@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
EVENT_SHOPPING_LIST_UPDATED, EVENT_SHOPPING_LIST_UPDATED,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED, EVENT_THEMES_UPDATED,
EVENT_LABEL_REGISTRY_UPDATED,
EVENT_CATEGORY_REGISTRY_UPDATED,
EVENT_FLOOR_REGISTRY_UPDATED,
} }

View File

@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError):
Will not be raised when validating authentication. Will not be raised when validating authentication.
""" """
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
def __init__( def __init__(
self, self,
*args: object, *args: object,
@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser):
) )
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
class Data: class Data:
"""Hold the user data.""" """Hold the user data."""
@ -216,7 +216,7 @@ class Data:
break break
if index is None: if index is None:
raise InvalidUser raise InvalidUser(translation_key="user_not_found")
self.users.pop(index) self.users.pop(index)
@ -232,7 +232,7 @@ class Data:
user["password"] = self.hash_password(new_password, True).decode() user["password"] = self.hash_password(new_password, True).decode()
break break
else: else:
raise InvalidUser raise InvalidUser(translation_key="user_not_found")
@callback @callback
def _validate_new_username(self, new_username: str) -> None: def _validate_new_username(self, new_username: str) -> None:
@ -275,7 +275,7 @@ class Data:
self._async_check_for_not_normalized_usernames(self._data) self._async_check_for_not_normalized_usernames(self._data)
break break
else: else:
raise InvalidUser raise InvalidUser(translation_key="user_not_found")
async def async_save(self) -> None: async def async_save(self) -> None:
"""Save data.""" """Save data."""

View File

@ -8,6 +8,8 @@ import glob
from http.client import HTTPConnection from http.client import HTTPConnection
import importlib import importlib
import os import os
from pathlib import Path
from ssl import SSLContext
import sys import sys
import threading import threading
import time import time
@ -143,6 +145,78 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
strict_core=False, strict_core=False,
skip_for_tests=True, skip_for_tests=True,
), ),
BlockingCall(
original_func=SSLContext.load_default_certs,
object=SSLContext,
function="load_default_certs",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.load_verify_locations,
object=SSLContext,
function="load_verify_locations",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.load_cert_chain,
object=SSLContext,
function="load_cert_chain",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.open,
object=Path,
function="open",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.read_text,
object=Path,
function="read_text",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.read_bytes,
object=Path,
function="read_bytes",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.write_text,
object=Path,
function="write_text",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.write_bytes,
object=Path,
function="write_bytes",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
) )

View File

@ -8,7 +8,7 @@ import contextlib
from functools import partial from functools import partial
from itertools import chain from itertools import chain
import logging import logging
import logging.handlers from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
import mimetypes import mimetypes
from operator import contains, itemgetter from operator import contains, itemgetter
import os import os
@ -88,7 +88,7 @@ from .helpers import (
) )
from .helpers.dispatcher import async_dispatcher_send_internal from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info from .helpers.system_info import async_get_system_info, is_official_image
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .setup import ( from .setup import (
# _setup_started is marked as protected to make it clear # _setup_started is marked as protected to make it clear
@ -104,7 +104,7 @@ from .setup import (
from .util.async_ import create_eager_task from .util.async_ import create_eager_task
from .util.hass_dict import HassKey from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_virtual_env from .util.package import async_get_user_site, is_docker_env, is_virtual_env
with contextlib.suppress(ImportError): with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop # Ensure anyio backend is imported to avoid it being imported in the event loop
@ -223,8 +223,10 @@ CRITICAL_INTEGRATIONS = {
SETUP_ORDER = ( SETUP_ORDER = (
# Load logging and http deps as soon as possible # Load logging and http deps as soon as possible
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS), ("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
# Setup frontend and recorder # Setup frontend
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), ("frontend", FRONTEND_INTEGRATIONS),
# Setup recorder
("recorder", RECORDER_INTEGRATIONS),
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.
("debugger", DEBUGGER_INTEGRATIONS), ("debugger", DEBUGGER_INTEGRATIONS),
) )
@ -257,12 +259,12 @@ async def async_setup_hass(
) -> core.HomeAssistant | None: ) -> core.HomeAssistant | None:
"""Set up Home Assistant.""" """Set up Home Assistant."""
def create_hass() -> core.HomeAssistant: async def create_hass() -> core.HomeAssistant:
"""Create the hass object and do basic setup.""" """Create the hass object and do basic setup."""
hass = core.HomeAssistant(runtime_config.config_dir) hass = core.HomeAssistant(runtime_config.config_dir)
loader.async_setup(hass) loader.async_setup(hass)
async_enable_logging( await async_enable_logging(
hass, hass,
runtime_config.verbose, runtime_config.verbose,
runtime_config.log_rotate_days, runtime_config.log_rotate_days,
@ -287,7 +289,7 @@ async def async_setup_hass(
async with hass.timeout.async_timeout(10): async with hass.timeout.async_timeout(10):
await hass.async_stop() await hass.async_stop()
hass = create_hass() hass = await create_hass()
if runtime_config.skip_pip or runtime_config.skip_pip_packages: if runtime_config.skip_pip or runtime_config.skip_pip_packages:
_LOGGER.warning( _LOGGER.warning(
@ -326,13 +328,13 @@ async def async_setup_hass(
if config_dict is None: if config_dict is None:
recovery_mode = True recovery_mode = True
await stop_hass(hass) await stop_hass(hass)
hass = create_hass() hass = await create_hass()
elif not basic_setup_success: elif not basic_setup_success:
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode") _LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
recovery_mode = True recovery_mode = True
await stop_hass(hass) await stop_hass(hass)
hass = create_hass() hass = await create_hass()
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
_LOGGER.warning( _LOGGER.warning(
@ -345,7 +347,7 @@ async def async_setup_hass(
recovery_mode = True recovery_mode = True
await stop_hass(hass) await stop_hass(hass)
hass = create_hass() hass = await create_hass()
if old_logging: if old_logging:
hass.data[DATA_LOGGING] = old_logging hass.data[DATA_LOGGING] = old_logging
@ -407,6 +409,10 @@ def _init_blocking_io_modules_in_executor() -> None:
# Initialize the mimetypes module to avoid blocking calls # Initialize the mimetypes module to avoid blocking calls
# to the filesystem to load the mime.types file. # to the filesystem to load the mime.types file.
mimetypes.init() mimetypes.init()
# Initialize is_official_image and is_docker_env to avoid blocking calls
# to the filesystem.
is_official_image()
is_docker_env()
async def async_load_base_functionality(hass: core.HomeAssistant) -> None: async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
@ -523,8 +529,7 @@ async def async_from_config_dict(
return hass return hass
@core.callback async def async_enable_logging(
def async_enable_logging(
hass: core.HomeAssistant, hass: core.HomeAssistant,
verbose: bool = False, verbose: bool = False,
log_rotate_days: int | None = None, log_rotate_days: int | None = None,
@ -581,10 +586,10 @@ def async_enable_logging(
logging.getLogger("aiohttp.access").setLevel(logging.WARNING) logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger(None).exception( sys.excepthook = lambda *args: logging.getLogger().exception(
"Uncaught exception", exc_info=args "Uncaught exception", exc_info=args
) )
threading.excepthook = lambda args: logging.getLogger(None).exception( threading.excepthook = lambda args: logging.getLogger().exception(
"Uncaught thread exception", "Uncaught thread exception",
exc_info=( # type: ignore[arg-type] exc_info=( # type: ignore[arg-type]
args.exc_type, args.exc_type,
@ -607,28 +612,13 @@ def async_enable_logging(
if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK) not err_path_exists and os.access(err_dir, os.W_OK)
): ):
err_handler: ( err_handler = await hass.async_add_executor_job(
logging.handlers.RotatingFileHandler _create_log_file, err_log_path, log_rotate_days
| logging.handlers.TimedRotatingFileHandler
) )
if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when="midnight", backupCount=log_rotate_days
)
else:
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
err_log_path, backupCount=1
)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME)) err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger = logging.getLogger("") logger = logging.getLogger()
logger.addHandler(err_handler) logger.addHandler(err_handler)
logger.setLevel(logging.INFO if verbose else logging.WARNING) logger.setLevel(logging.INFO if verbose else logging.WARNING)
@ -640,7 +630,29 @@ def async_enable_logging(
async_activate_log_queue_handler(hass) async_activate_log_queue_handler(hass)
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): def _create_log_file(
err_log_path: str, log_rotate_days: int | None
) -> RotatingFileHandler | TimedRotatingFileHandler:
"""Create log file and do roll over."""
err_handler: RotatingFileHandler | TimedRotatingFileHandler
if log_rotate_days:
err_handler = TimedRotatingFileHandler(
err_log_path, when="midnight", backupCount=log_rotate_days
)
else:
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
err_log_path, backupCount=1
)
try:
err_handler.doRollover()
except OSError as err:
_LOGGER.error("Error rolling over log file: %s", err)
return err_handler
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
"""RotatingFileHandler that does not check if it should roll over on every log.""" """RotatingFileHandler that does not check if it should roll over on every log."""
def shouldRollover(self, record: logging.LogRecord) -> bool: def shouldRollover(self, record: logging.LogRecord) -> bool:
@ -895,7 +907,13 @@ async def _async_resolve_domains_to_setup(
await asyncio.gather(*resolve_dependencies_tasks) await asyncio.gather(*resolve_dependencies_tasks)
for itg in integrations_to_process: for itg in integrations_to_process:
for dep in itg.all_dependencies: try:
all_deps = itg.all_dependencies
except RuntimeError:
# Integration.all_dependencies raises RuntimeError if
# dependencies could not be resolved
continue
for dep in all_deps:
if dep in domains_to_setup: if dep in domains_to_setup:
continue continue
domains_to_setup.add(dep) domains_to_setup.add(dep)

View File

@ -1,5 +0,0 @@
{
"domain": "asterisk",
"name": "Asterisk",
"integrations": ["asterisk_cdr", "asterisk_mbox"]
}

View File

@ -0,0 +1,5 @@
{
"domain": "fujitsu",
"name": "Fujitsu",
"integrations": ["fujitsu_anywair", "fujitsu_fglair"]
}

View File

@ -1,5 +1,5 @@
{ {
"domain": "logitech", "domain": "logitech",
"name": "Logitech", "name": "Logitech",
"integrations": ["harmony", "ue_smart_radio", "squeezebox"] "integrations": ["harmony", "squeezebox"]
} }

View File

@ -1,5 +1,5 @@
{ {
"domain": "tesla", "domain": "tesla",
"name": "Tesla", "name": "Tesla",
"integrations": ["powerwall", "tesla_wall_connector"] "integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"]
} }

View File

@ -9,5 +9,5 @@
}, },
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"], "loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==5.1.2"] "requirements": ["jaraco.abode==5.2.1"]
} }

View File

@ -81,7 +81,7 @@ class AcerSwitch(SwitchEntity):
write_timeout: int, write_timeout: int,
) -> None: ) -> None:
"""Init of the Acer projector.""" """Init of the Acer projector."""
self.ser = serial.Serial( self.serial = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout port=serial_port, timeout=timeout, write_timeout=write_timeout
) )
self._serial_port = serial_port self._serial_port = serial_port
@ -99,16 +99,16 @@ class AcerSwitch(SwitchEntity):
# was disconnected during runtime. # was disconnected during runtime.
# This way the projector can be reconnected and will still work # This way the projector can be reconnected and will still work
try: try:
if not self.ser.is_open: if not self.serial.is_open:
self.ser.open() self.serial.open()
self.ser.write(msg.encode("utf-8")) self.serial.write(msg.encode("utf-8"))
# Size is an experience value there is no real limit. # Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually # AFAIK there is no limit and no end character so we will usually
# need to wait for timeout # need to wait for timeout
ret = self.ser.read_until(size=20).decode("utf-8") ret = self.serial.read_until(size=20).decode("utf-8")
except serial.SerialException: except serial.SerialException:
_LOGGER.error("Problem communicating with %s", self._serial_port) _LOGGER.error("Problem communicating with %s", self._serial_port)
self.ser.close() self.serial.close()
return ret return ret
def _write_read_format(self, msg: str) -> str: def _write_read_format(self, msg: str) -> str:

View File

@ -136,7 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Tuple to hold data needed for notification # Tuple to hold data needed for notification
NotificationItem = namedtuple( NotificationItem = namedtuple( # noqa: PYI024
"NotificationItem", "hnotify huser name plc_datatype callback" "NotificationItem", "hnotify huser name plc_datatype callback"
) )

View File

@ -206,7 +206,8 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC Mode and State.""" """Set the HVAC Mode and State."""
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
return await self.async_turn_off() await self.async_turn_off()
return
if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO: if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
raise ServiceValidationError("Heat/Cool is not supported in this mode") raise ServiceValidationError("Heat/Cool is not supported in this mode")
await self.async_update_ac( await self.async_update_ac(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aemet_opendata"], "loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.5.2"] "requirements": ["AEMET-OpenData==0.5.4"]
} }

View File

@ -59,7 +59,7 @@ async def async_setup_entry(
platform = async_get_current_platform() platform = async_get_current_platform()
for service, method in CAMERA_SERVICES.items(): for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, {}, method) platform.async_register_entity_service(service, None, method)
class AgentCamera(MjpegCamera): class AgentCamera(MjpegCamera):

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from airgradient import AirGradientClient from airgradient import AirGradientClient, get_model_name
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, Platform
@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.UPDATE,
] ]
@ -35,7 +36,7 @@ class AirGradientData:
type AirGradientConfigEntry = ConfigEntry[AirGradientData] type AirGradientConfigEntry = ConfigEntry[AirGradientData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
"""Set up Airgradient from a config entry.""" """Set up Airgradient from a config entry."""
client = AirGradientClient( client = AirGradientClient(
@ -53,7 +54,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, measurement_coordinator.serial_number)}, identifiers={(DOMAIN, measurement_coordinator.serial_number)},
manufacturer="AirGradient", manufacturer="AirGradient",
model=measurement_coordinator.data.model, model=get_model_name(measurement_coordinator.data.model),
model_id=measurement_coordinator.data.model,
serial_number=measurement_coordinator.data.serial_number, serial_number=measurement_coordinator.data.serial_number,
sw_version=measurement_coordinator.data.firmware_version, sw_version=measurement_coordinator.data.firmware_version,
) )
@ -68,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: AirGradientConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -2,9 +2,13 @@
from typing import Any from typing import Any
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl from airgradient import (
AirGradientClient,
AirGradientError,
AirGradientParseError,
ConfigurationControl,
)
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from mashumaro import MissingField
import voluptuous as vol import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
@ -83,12 +87,14 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
self.client = AirGradientClient(user_input[CONF_HOST], session=session) self.client = AirGradientClient(user_input[CONF_HOST], session=session)
try: try:
current_measures = await self.client.get_current_measures() current_measures = await self.client.get_current_measures()
except AirGradientParseError:
return self.async_abort(reason="invalid_version")
except AirGradientError: except AirGradientError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except MissingField:
return self.async_abort(reason="invalid_version")
else: else:
await self.async_set_unique_id(current_measures.serial_number) await self.async_set_unique_id(
current_measures.serial_number, raise_on_progress=False
)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
await self.set_configuration_source() await self.set_configuration_source()
return self.async_create_entry( return self.async_create_entry(

View File

@ -19,7 +19,6 @@ if TYPE_CHECKING:
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Class to manage fetching AirGradient data.""" """Class to manage fetching AirGradient data."""
_update_interval: timedelta
config_entry: AirGradientConfigEntry config_entry: AirGradientConfigEntry
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None: def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
@ -28,7 +27,7 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
hass, hass,
logger=LOGGER, logger=LOGGER,
name=f"AirGradient {client.host}", name=f"AirGradient {client.host}",
update_interval=self._update_interval, update_interval=timedelta(minutes=1),
) )
self.client = client self.client = client
assert self.config_entry.unique_id assert self.config_entry.unique_id
@ -47,8 +46,6 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]): class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
"""Class to manage fetching AirGradient data.""" """Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=1)
async def _update_data(self) -> Measures: async def _update_data(self) -> Measures:
return await self.client.get_current_measures() return await self.client.get_current_measures()
@ -56,7 +53,5 @@ class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]): class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
"""Class to manage fetching AirGradient data.""" """Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=5)
async def _update_data(self) -> Config: async def _update_data(self) -> Config:
return await self.client.get_config() return await self.client.get_config()

View File

@ -8,6 +8,34 @@
"default": "mdi:lightbulb-on-outline" "default": "mdi:lightbulb-on-outline"
} }
}, },
"number": {
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
}
},
"select": {
"configuration_control": {
"default": "mdi:cloud-cog"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"sensor": { "sensor": {
"total_volatile_organic_component_index": { "total_volatile_organic_component_index": {
"default": "mdi:molecule" "default": "mdi:molecule"
@ -17,6 +45,32 @@
}, },
"pm003_count": { "pm003_count": {
"default": "mdi:blur" "default": "mdi:blur"
},
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"switch": {
"post_data_to_airgradient": {
"default": "mdi:cogs"
} }
} }
} }

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient", "documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airgradient==0.6.0"], "requirements": ["airgradient==0.8.0"],
"zeroconf": ["_airgradient._tcp.local."] "zeroconf": ["_airgradient._tcp.local."]
} }

View File

@ -16,6 +16,7 @@
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1." "invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
}, },
"error": { "error": {

View File

@ -0,0 +1,55 @@
"""Airgradient Update platform."""
from datetime import timedelta
from functools import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
SCAN_INTERVAL = timedelta(hours=1)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirGradientConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Airgradient update platform."""
data = config_entry.runtime_data
async_add_entities([AirGradientUpdate(data.measurement)], True)
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
coordinator: AirGradientMeasurementCoordinator
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-update"
@cached_property
def should_poll(self) -> bool:
"""Return True because we need to poll the latest version."""
return True
@property
def installed_version(self) -> str:
"""Return the installed version of the entity."""
return self.coordinator.data.firmware_version
async def async_update(self) -> None:
"""Update the entity."""
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
)

View File

@ -156,7 +156,8 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
return await self.async_turn_off() await self.async_turn_off()
return
await self._airtouch.SetCoolingModeForAc( await self._airtouch.SetCoolingModeForAc(
self._ac_number, HA_STATE_TO_AT[hvac_mode] self._ac_number, HA_STATE_TO_AT[hvac_mode]
) )
@ -262,7 +263,8 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
return await self.async_turn_off() await self.async_turn_off()
return
if self.hvac_mode == HVACMode.OFF: if self.hvac_mode == HVACMode.OFF:
await self.async_turn_on() await self.async_turn_on()
self._unit = self._airtouch.GetGroups()[self._group_number] self._unit = self._airtouch.GetGroups()[self._group_number]

View File

@ -1,9 +1,11 @@
"""Config flow for AirTouch4.""" """Config flow for AirTouch4."""
from typing import Any
from airtouch4pyapi import AirTouch, AirTouchStatus from airtouch4pyapi import AirTouch, AirTouchStatus
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from .const import DOMAIN from .const import DOMAIN
@ -16,7 +18,9 @@ class AirtouchConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if user_input is None: if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)

View File

@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE] PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient] type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]

View File

@ -121,6 +121,7 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
"""Base class for Airtouch5 Climate Entities.""" """Base class for Airtouch5 Climate Entities."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
_attr_target_temperature_step = 1 _attr_target_temperature_step = 1
_attr_name = None _attr_name = None
_enable_turn_on_off_backwards_compatibility = False _enable_turn_on_off_backwards_compatibility = False

View File

@ -0,0 +1,134 @@
"""Representation of the Damper for AirTouch 5 Devices."""
import logging
from typing import Any
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from airtouch5py.packets.zone_control import (
ZoneControlZone,
ZoneSettingPower,
ZoneSettingValue,
)
from airtouch5py.packets.zone_name import ZoneName
from airtouch5py.packets.zone_status import ZoneStatusZone
from homeassistant.components.cover import (
ATTR_POSITION,
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import Airtouch5ConfigEntry
from .const import DOMAIN
from .entity import Airtouch5Entity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: Airtouch5ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airtouch 5 Cover entities."""
client = config_entry.runtime_data
# Each zone has a cover for its open percentage
async_add_entities(
Airtouch5ZoneOpenPercentage(
client, zone, client.latest_zone_status[zone.zone_number].has_sensor
)
for zone in client.zones
)
class Airtouch5ZoneOpenPercentage(CoverEntity, Airtouch5Entity):
"""How open the damper is in each zone."""
_attr_device_class = CoverDeviceClass.DAMPER
_attr_translation_key = "damper"
# Zones with temperature sensors shouldn't be manually controlled.
# We allow it but warn the user in the integration documentation.
_attr_supported_features = (
CoverEntityFeature.SET_POSITION
| CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
)
def __init__(
self, client: Airtouch5SimpleClient, zone_name: ZoneName, has_sensor: bool
) -> None:
"""Initialise the Cover Entity."""
super().__init__(client)
self._zone_name = zone_name
self._attr_unique_id = f"zone_{zone_name.zone_number}_open_percentage"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"zone_{zone_name.zone_number}")},
name=zone_name.zone_name,
manufacturer="Polyaire",
model="AirTouch 5",
)
@callback
def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None:
if self._zone_name.zone_number not in data:
return
status = data[self._zone_name.zone_number]
self._attr_current_cover_position = int(status.open_percentage * 100)
if status.open_percentage == 0:
self._attr_is_closed = True
else:
self._attr_is_closed = False
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Add data updated listener after this object has been initialized."""
await super().async_added_to_hass()
self._client.zone_status_callbacks.append(self._async_update_attrs)
self._async_update_attrs(self._client.latest_zone_status)
async def async_will_remove_from_hass(self) -> None:
"""Remove data updated listener after this object has been initialized."""
await super().async_will_remove_from_hass()
self._client.zone_status_callbacks.remove(self._async_update_attrs)
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the damper."""
await self._set_cover_position(100)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close damper."""
await self._set_cover_position(0)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Update the damper to a specific position."""
if (position := kwargs.get(ATTR_POSITION)) is None:
_LOGGER.debug("Argument `position` is missing in set_cover_position")
return
await self._set_cover_position(position)
async def _set_cover_position(self, position_percent: float) -> None:
power: ZoneSettingPower
if position_percent == 0:
power = ZoneSettingPower.SET_TO_OFF
else:
power = ZoneSettingPower.SET_TO_ON
zcz = ZoneControlZone(
self._zone_name.zone_number,
ZoneSettingValue.SET_OPEN_PERCENTAGE,
power,
position_percent / 100.0,
)
packet = self._client.data_packet_factory.zone_control([zcz])
await self._client.send_packet(packet)

View File

@ -6,15 +6,12 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class Airtouch5Entity(Entity): class Airtouch5Entity(Entity):
"""Base class for Airtouch5 entities.""" """Base class for Airtouch5 entities."""
_attr_should_poll = False _attr_should_poll = False
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_translation_key = DOMAIN
def __init__(self, client: Airtouch5SimpleClient) -> None: def __init__(self, client: Airtouch5SimpleClient) -> None:
"""Initialise the Entity.""" """Initialise the Entity."""

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5", "documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["airtouch5py"], "loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.2.8"] "requirements": ["airtouch5py==0.2.10"]
} }

View File

@ -27,6 +27,11 @@
} }
} }
} }
},
"cover": {
"damper": {
"name": "[%key:component::cover::entity_component::damper::name%]"
}
} }
} }
} }

View File

@ -31,7 +31,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import ( from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
config_validation as cv,
device_registry as dr, device_registry as dr,
entity_registry as er, entity_registry as er,
) )
@ -62,8 +61,6 @@ PLATFORMS = [Platform.SENSOR]
DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@callback @callback
def async_get_cloud_api_update_interval( def async_get_cloud_api_update_interval(

View File

@ -82,33 +82,54 @@ async def async_setup_entry(
"""Add Airzone binary sensors from a config_entry.""" """Add Airzone binary sensors from a config_entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
binary_sensors: list[AirzoneBinarySensor] = [ added_systems: set[str] = set()
AirzoneSystemBinarySensor( added_zones: set[str] = set()
coordinator,
description,
entry,
system_id,
system_data,
)
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in system_data
]
binary_sensors.extend( def _async_entity_listener() -> None:
AirzoneZoneBinarySensor( """Handle additions of binary sensors."""
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zone_data
)
async_add_entities(binary_sensors) entities: list[AirzoneBinarySensor] = []
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemBinarySensor(
coordinator,
description,
entry,
system_id,
systems_data.get(system_id),
)
for system_id in new_systems
for description in SYSTEM_BINARY_SENSOR_TYPES
if description.key in systems_data.get(system_id)
)
added_systems.update(new_systems)
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities.extend(
AirzoneZoneBinarySensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_BINARY_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):

View File

@ -102,17 +102,31 @@ async def async_setup_entry(
entry: AirzoneConfigEntry, entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add Airzone sensors from a config_entry.""" """Add Airzone climate from a config_entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities(
AirzoneClimate( added_zones: set[str] = set()
coordinator,
entry, def _async_entity_listener() -> None:
system_zone_id, """Handle additions of climate."""
zone_data,
) zones_data = coordinator.data.get(AZD_ZONES, {})
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() received_zones = set(zones_data)
) new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):

View File

@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
) )
try: try:
await airzone.get_version() await airzone.get_version()
except AirzoneError as err: except (AirzoneError, TimeoutError) as err:
raise AbortFlow("cannot_connect") from err raise AbortFlow("cannot_connect") from err
return await self.async_step_discovered_connection() return await self.async_step_discovered_connection()

View File

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

View File

@ -2,16 +2,21 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Final from typing import Any, Final
from aioairzone.common import GrilleAngle, SleepTimeout from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
from aioairzone.const import ( from aioairzone.const import (
API_COLD_ANGLE, API_COLD_ANGLE,
API_HEAT_ANGLE, API_HEAT_ANGLE,
API_MODE,
API_SLEEP, API_SLEEP,
AZD_COLD_ANGLE, AZD_COLD_ANGLE,
AZD_HEAT_ANGLE, AZD_HEAT_ANGLE,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_SLEEP, AZD_SLEEP,
AZD_ZONES, AZD_ZONES,
) )
@ -33,6 +38,9 @@ class AirzoneSelectDescription(SelectEntityDescription):
api_param: str api_param: str
options_dict: dict[str, int] options_dict: dict[str, int]
options_fn: Callable[[dict[str, Any], dict[str, int]], list[str]] = (
lambda zone_data, value: list(value)
)
GRILLE_ANGLE_DICT: Final[dict[str, int]] = { GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
@ -42,6 +50,15 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
"40deg": GrilleAngle.DEG_40, "40deg": GrilleAngle.DEG_40,
} }
MODE_DICT: Final[dict[str, int]] = {
"cool": OperationMode.COOLING,
"dry": OperationMode.DRY,
"fan": OperationMode.FAN,
"heat": OperationMode.HEATING,
"heat_cool": OperationMode.AUTO,
"stop": OperationMode.STOP,
}
SLEEP_DICT: Final[dict[str, int]] = { SLEEP_DICT: Final[dict[str, int]] = {
"off": SleepTimeout.SLEEP_OFF, "off": SleepTimeout.SLEEP_OFF,
"30m": SleepTimeout.SLEEP_30, "30m": SleepTimeout.SLEEP_30,
@ -50,6 +67,26 @@ SLEEP_DICT: Final[dict[str, int]] = {
} }
def main_zone_options(
zone_data: dict[str, Any],
options: dict[str, int],
) -> list[str]:
"""Filter available modes."""
modes = zone_data.get(AZD_MODES, [])
return [k for k, v in options.items() if v in modes]
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_MODE,
key=AZD_MODE,
options_dict=MODE_DICT,
options_fn=main_zone_options,
translation_key="modes",
),
)
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription( AirzoneSelectDescription(
api_param=API_COLD_ANGLE, api_param=API_COLD_ANGLE,
@ -83,21 +120,48 @@ async def async_setup_entry(
entry: AirzoneConfigEntry, entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add Airzone sensors from a config_entry.""" """Add Airzone select from a config_entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( added_zones: set[str] = set()
AirzoneZoneSelect(
coordinator, def _async_entity_listener() -> None:
description, """Handle additions of select."""
entry,
system_zone_id, zones_data = coordinator.data.get(AZD_ZONES, {})
zone_data, received_zones = set(zones_data)
) new_zones = received_zones - added_zones
for description in ZONE_SELECT_TYPES if new_zones:
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() entities: list[AirzoneZoneSelect] = [
if description.key in zone_data AirzoneZoneSelect(
) coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in MAIN_ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
]
entities += [
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
]
async_add_entities(entities)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
class AirzoneBaseSelect(AirzoneEntity, SelectEntity): class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
@ -140,6 +204,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
f"{self._attr_unique_id}_{system_zone_id}_{description.key}" f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
) )
self.entity_description = description self.entity_description = description
self._attr_options = self.entity_description.options_fn(
zone_data, description.options_dict
)
self.values_dict = {v: k for k, v in description.options_dict.items()} self.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs() self._async_update_attrs()

View File

@ -85,21 +85,37 @@ async def async_setup_entry(
"""Add Airzone sensors from a config_entry.""" """Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
sensors: list[AirzoneSensor] = [ added_zones: set[str] = set()
AirzoneZoneSensor(
coordinator, def _async_entity_listener() -> None:
description, """Handle additions of sensors."""
entry,
system_zone_id, entities: list[AirzoneSensor] = []
zone_data,
) zones_data = coordinator.data.get(AZD_ZONES, {})
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() received_zones = set(zones_data)
for description in ZONE_SENSOR_TYPES new_zones = received_zones - added_zones
if description.key in zone_data if new_zones:
] entities.extend(
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SENSOR_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
async_add_entities(entities)
entities: list[AirzoneSensor] = []
if AZD_HOT_WATER in coordinator.data: if AZD_HOT_WATER in coordinator.data:
sensors.extend( entities.extend(
AirzoneHotWaterSensor( AirzoneHotWaterSensor(
coordinator, coordinator,
description, description,
@ -110,7 +126,7 @@ async def async_setup_entry(
) )
if AZD_WEBSERVER in coordinator.data: if AZD_WEBSERVER in coordinator.data:
sensors.extend( entities.extend(
AirzoneWebServerSensor( AirzoneWebServerSensor(
coordinator, coordinator,
description, description,
@ -120,7 +136,10 @@ async def async_setup_entry(
if description.key in coordinator.data[AZD_WEBSERVER] if description.key in coordinator.data[AZD_WEBSERVER]
) )
async_add_entities(sensors) async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
class AirzoneSensor(AirzoneEntity, SensorEntity): class AirzoneSensor(AirzoneEntity, SensorEntity):

View File

@ -52,6 +52,17 @@
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]" "40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
} }
}, },
"modes": {
"name": "Mode",
"state": {
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
"stop": "Stop"
}
},
"sleep_times": { "sleep_times": {
"name": "Sleep", "name": "Sleep",
"state": { "state": {

View File

@ -61,7 +61,7 @@ async def async_setup_entry(
entry: AirzoneConfigEntry, entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add Airzone sensors from a config_entry.""" """Add Airzone Water Heater from a config_entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
if AZD_HOT_WATER in coordinator.data: if AZD_HOT_WATER in coordinator.data:
async_add_entities([AirzoneWaterHeater(coordinator, entry)]) async_add_entities([AirzoneWaterHeater(coordinator, entry)])

View File

@ -14,6 +14,7 @@ from aioairzone_cloud.const import (
AZD_FLOOR_DEMAND, AZD_FLOOR_DEMAND,
AZD_PROBLEMS, AZD_PROBLEMS,
AZD_SYSTEMS, AZD_SYSTEMS,
AZD_THERMOSTAT_BATTERY_LOW,
AZD_WARNINGS, AZD_WARNINGS,
AZD_ZONES, AZD_ZONES,
) )
@ -88,6 +89,10 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]
key=AZD_AQ_ACTIVE, key=AZD_AQ_ACTIVE,
translation_key="air_quality_active", translation_key="air_quality_active",
), ),
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY_LOW,
),
AirzoneBinarySensorEntityDescription( AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING, device_class=BinarySensorDeviceClass.RUNNING,
key=AZD_FLOOR_DEMAND, key=AZD_FLOOR_DEMAND,
@ -156,6 +161,11 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
entity_description: AirzoneBinarySensorEntityDescription entity_description: AirzoneBinarySensorEntityDescription
@property
def available(self) -> bool:
"""Return Airzone Cloud binary sensor availability."""
return super().available and self.is_on is not None
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates.""" """Update attributes when the coordinator updates."""

View File

@ -13,9 +13,12 @@ from aioairzone_cloud.const import (
AZD_GROUPS, AZD_GROUPS,
AZD_HOT_WATERS, AZD_HOT_WATERS,
AZD_INSTALLATIONS, AZD_INSTALLATIONS,
AZD_MODEL,
AZD_NAME, AZD_NAME,
AZD_SYSTEM_ID, AZD_SYSTEM_ID,
AZD_SYSTEMS, AZD_SYSTEMS,
AZD_THERMOSTAT_FW,
AZD_THERMOSTAT_MODEL,
AZD_WEBSERVER, AZD_WEBSERVER,
AZD_WEBSERVERS, AZD_WEBSERVERS,
AZD_ZONES, AZD_ZONES,
@ -69,6 +72,7 @@ class AirzoneAidooEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, aidoo_id)}, identifiers={(DOMAIN, aidoo_id)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=aidoo_data[AZD_MODEL],
name=aidoo_data[AZD_NAME], name=aidoo_data[AZD_NAME],
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]), via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
) )
@ -111,6 +115,7 @@ class AirzoneGroupEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, group_id)}, identifiers={(DOMAIN, group_id)},
model="Group",
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
name=group_data[AZD_NAME], name=group_data[AZD_NAME],
) )
@ -154,6 +159,7 @@ class AirzoneHotWaterEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, dhw_id)}, identifiers={(DOMAIN, dhw_id)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model="Hot Water",
name=dhw_data[AZD_NAME], name=dhw_data[AZD_NAME],
via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]), via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]),
) )
@ -195,6 +201,7 @@ class AirzoneInstallationEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, inst_id)}, identifiers={(DOMAIN, inst_id)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model="Installation",
name=inst_data[AZD_NAME], name=inst_data[AZD_NAME],
) )
@ -240,9 +247,11 @@ class AirzoneSystemEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_id)}, identifiers={(DOMAIN, system_id)},
model=system_data.get(AZD_MODEL),
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
name=system_data[AZD_NAME], name=system_data[AZD_NAME],
via_device=(DOMAIN, system_data[AZD_WEBSERVER]), via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
sw_version=system_data.get(AZD_FIRMWARE),
) )
def get_airzone_value(self, key: str) -> Any: def get_airzone_value(self, key: str) -> Any:
@ -270,6 +279,7 @@ class AirzoneWebServerEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)}, connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
identifiers={(DOMAIN, ws_id)}, identifiers={(DOMAIN, ws_id)},
model="WebServer",
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
name=ws_data[AZD_NAME], name=ws_data[AZD_NAME],
sw_version=ws_data[AZD_FIRMWARE], sw_version=ws_data[AZD_FIRMWARE],
@ -300,9 +310,11 @@ class AirzoneZoneEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, zone_id)}, identifiers={(DOMAIN, zone_id)},
model=zone_data.get(AZD_THERMOSTAT_MODEL),
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
name=zone_data[AZD_NAME], name=zone_data[AZD_NAME],
via_device=(DOMAIN, self.system_id), via_device=(DOMAIN, self.system_id),
sw_version=zone_data.get(AZD_THERMOSTAT_FW),
) )
def get_airzone_value(self, key: str) -> Any: def get_airzone_value(self, key: str) -> Any:

View File

@ -0,0 +1,15 @@
{
"entity": {
"sensor": {
"cpu_usage": {
"default": "mdi:cpu-32-bit"
},
"free_memory": {
"default": "mdi:memory"
},
"thermostat_coverage": {
"default": "mdi:signal"
}
}
}
}

View File

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

View File

@ -10,8 +10,12 @@ from aioairzone_cloud.const import (
AZD_AQ_PM_1, AZD_AQ_PM_1,
AZD_AQ_PM_2P5, AZD_AQ_PM_2P5,
AZD_AQ_PM_10, AZD_AQ_PM_10,
AZD_CPU_USAGE,
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_MEMORY_FREE,
AZD_TEMP, AZD_TEMP,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_COVERAGE,
AZD_WEBSERVERS, AZD_WEBSERVERS,
AZD_WIFI_RSSI, AZD_WIFI_RSSI,
AZD_ZONES, AZD_ZONES,
@ -28,6 +32,7 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory, EntityCategory,
UnitOfInformation,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -52,6 +57,22 @@ AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
) )
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_CPU_USAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="cpu_usage",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_MEMORY_FREE,
native_unit_of_measurement=UnitOfInformation.BYTES,
state_class=SensorStateClass.MEASUREMENT,
translation_key="free_memory",
),
SensorEntityDescription( SensorEntityDescription(
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
@ -98,6 +119,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription(
device_class=SensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_THERMOSTAT_COVERAGE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="thermostat_coverage",
),
) )
@ -154,6 +189,11 @@ async def async_setup_entry(
class AirzoneSensor(AirzoneEntity, SensorEntity): class AirzoneSensor(AirzoneEntity, SensorEntity):
"""Define an Airzone Cloud sensor.""" """Define an Airzone Cloud sensor."""
@property
def available(self) -> bool:
"""Return Airzone Cloud sensor availability."""
return super().available and self.native_value is not None
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates.""" """Update attributes when the coordinator updates."""

View File

@ -37,6 +37,17 @@
"auto": "Auto" "auto": "Auto"
} }
} }
},
"sensor": {
"cpu_usage": {
"name": "CPU usage"
},
"free_memory": {
"name": "Free memory"
},
"thermostat_coverage": {
"name": "Signal percentage"
}
} }
} }
} }

View File

@ -1,94 +1,38 @@
"""The Aladdin Connect Genie integration.""" """The Aladdin Connect Genie integration."""
# mypy: ignore-errors
from __future__ import annotations from __future__ import annotations
# from genie_partner_sdk.client import AladdinConnectClient from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from .api import AsyncConfigEntryAuth DOMAIN = "aladdin_connect"
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
hass: HomeAssistant, entry: AladdinConnectConfigEntry """Set up Aladdin Connect from a config entry."""
) -> bool: ir.async_create_issue(
"""Set up Aladdin Connect Genie from a config entry.""" hass,
implementation = await async_get_config_entry_implementation(hass, entry) DOMAIN,
DOMAIN,
session = OAuth2Session(hass, entry, implementation) is_fixable=False,
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) severity=ir.IssueSeverity.ERROR,
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth)) translation_key="integration_removed",
translation_placeholders={
await coordinator.async_setup() "entries": "/config/integrations/integration/aladdin_connect",
await coordinator.async_config_entry_first_refresh() },
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async_remove_stale_devices(hass, entry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> bool:
"""Migrate old config."""
if config_entry.version < 2:
config_entry.async_start_reauth(hass)
hass.config_entries.async_update_entry(
config_entry,
version=2,
minor_version=1,
)
return True
def async_remove_stale_devices(
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
) -> None:
"""Remove stale devices from device registry."""
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
) )
all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors}
for device_entry in device_entries: return True
device_id: str | None = None
for identifier in device_entry.identifiers:
if identifier[0] == DOMAIN:
device_id = identifier[1]
break
if device_id is None or device_id not in all_device_ids: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# If device_id is None an invalid device entry was found for this config entry. """Unload a config entry."""
# If the device_id is not in existing device ids it's a stale device entry. if all(
# Remove config entry from this device entry in either case. config_entry.state is ConfigEntryState.NOT_LOADED
device_registry.async_update_device( for config_entry in hass.config_entries.async_entries(DOMAIN)
device_entry.id, remove_config_entry_id=config_entry.entry_id if config_entry.entry_id != entry.entry_id
) ):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
return True

View File

@ -1,33 +0,0 @@
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
# mypy: ignore-errors
from typing import cast
from aiohttp import ClientSession
# from genie_partner_sdk.auth import Auth
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: OAuth2Session,
) -> None:
"""Initialize Aladdin Connect Genie auth."""
super().__init__(
websession, API_URL, oauth_session.token["access_token"], API_KEY
)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
await self._oauth_session.async_ensure_token_valid()
return cast(str, self._oauth_session.token["access_token"])

View File

@ -1,70 +1,11 @@
"""Config flow for Aladdin Connect Genie.""" """Config flow for Aladdin Connect integration."""
from collections.abc import Mapping from homeassistant.config_entries import ConfigFlow
import logging
from typing import Any
import jwt from . import DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DOMAIN
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication.""" """Handle a config flow for Aladdin Connect."""
DOMAIN = DOMAIN VERSION = 1
VERSION = 2
MINOR_VERSION = 1
reauth_entry: ConfigEntry | None = None
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create an oauth config entry or update existing entry for reauth."""
token_payload = jwt.decode(
data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False}
)
if not self.reauth_entry:
await self.async_set_unique_id(token_payload["sub"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=token_payload["username"],
data=data,
)
if self.reauth_entry.unique_id == token_payload["username"]:
return self.async_update_reload_and_abort(
self.reauth_entry,
data=data,
unique_id=token_payload["sub"],
)
if self.reauth_entry.unique_id == token_payload["sub"]:
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
return self.async_abort(reason="wrong_account")
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

View File

@ -1,6 +0,0 @@
"""Constants for the Aladdin Connect Genie integration."""
DOMAIN = "aladdin_connect"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"

View File

@ -1,38 +0,0 @@
"""Define an object to coordinate fetching Aladdin Connect data."""
# mypy: ignore-errors
from datetime import timedelta
import logging
# from genie_partner_sdk.client import AladdinConnectClient
# from genie_partner_sdk.model import GarageDoor
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AladdinConnectCoordinator(DataUpdateCoordinator[None]):
"""Aladdin Connect Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None:
"""Initialize."""
super().__init__(
hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=15),
)
self.acc = acc
self.doors: list[GarageDoor] = []
async def async_setup(self) -> None:
"""Fetch initial data."""
self.doors = await self.acc.get_doors()
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
for door in self.doors:
await self.acc.update_door(door.device_id, door.door_number)

View File

@ -1,84 +0,0 @@
"""Cover Entity for Genie Garage Door."""
# mypy: ignore-errors
from typing import Any
# from genie_partner_sdk.model import GarageDoor
from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AladdinConnectConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aladdin Connect platform."""
coordinator = config_entry.runtime_data
async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors)
class AladdinDevice(AladdinConnectEntity, CoverEntity):
"""Representation of Aladdin Connect cover."""
_attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
_attr_name = None
def __init__(
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator, device)
self._attr_unique_id = device.unique_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
await self.coordinator.acc.open_door(
self._device.device_id, self._device.door_number
)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Issue close command to cover."""
await self.coordinator.acc.close_door(
self._device.device_id, self._device.door_number
)
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "closed")
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "closing")
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
value = self.coordinator.acc.get_door_status(
self._device.device_id, self._device.door_number
)
if value is None:
return None
return bool(value == "opening")

View File

@ -1,27 +0,0 @@
"""Defines a base Aladdin Connect entity."""
# mypy: ignore-errors
# from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
"""Defines a base Aladdin Connect entity."""
_attr_has_entity_name = True
def __init__(
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._device = device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
name=device.name,
manufacturer="Overhead Door",
)

View File

@ -1,11 +1,9 @@
{ {
"domain": "aladdin_connect", "domain": "aladdin_connect",
"name": "Aladdin Connect", "name": "Aladdin Connect",
"codeowners": ["@swcloudgenie"], "codeowners": [],
"config_flow": true,
"dependencies": ["application_credentials"],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "system",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["genie-partner-sdk==1.0.2"] "requirements": []
} }

View File

@ -1,80 +0,0 @@
"""Support for Aladdin Connect Garage Door sensors."""
# mypy: ignore-errors
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
# from genie_partner_sdk.client import AladdinConnectClient
# from genie_partner_sdk.model import GarageDoor
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
from .entity import AladdinConnectEntity
@dataclass(frozen=True, kw_only=True)
class AccSensorEntityDescription(SensorEntityDescription):
"""Describes AladdinConnect sensor entity."""
value_fn: Callable[[AladdinConnectClient, str, int], float | None]
SENSORS: tuple[AccSensorEntityDescription, ...] = (
AccSensorEntityDescription(
key="battery_level",
device_class=SensorDeviceClass.BATTERY,
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=AladdinConnectClient.get_battery_status,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinator = entry.runtime_data
async_add_entities(
AladdinConnectSensor(coordinator, door, description)
for description in SENSORS
for door in coordinator.doors
)
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
"""A sensor implementation for Aladdin Connect devices."""
entity_description: AccSensorEntityDescription
def __init__(
self,
coordinator: AladdinConnectCoordinator,
device: GarageDoor,
description: AccSensorEntityDescription,
) -> None:
"""Initialize a sensor for an Aladdin Connect device."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{device.unique_id}-{description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(
self.coordinator.acc, self._device.device_id, self._device.door_number
)

View File

@ -1,29 +1,8 @@
{ {
"config": { "issues": {
"step": { "integration_removed": {
"pick_implementation": { "title": "The Aladdin Connect integration has been removed",
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" "description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Aladdin Connect needs to re-authenticate your account"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
} }
} }
} }

View File

@ -124,9 +124,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not entities: if not entities:
return False return False
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off")
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle")
await component.async_add_entities(entities) await component.async_add_entities(entities)
@ -162,16 +162,8 @@ class Alert(Entity):
self._data = data self._data = data
self._message_template = message_template self._message_template = message_template
if self._message_template is not None:
self._message_template.hass = hass
self._done_message_template = done_message_template self._done_message_template = done_message_template
if self._done_message_template is not None:
self._done_message_template.hass = hass
self._title_template = title_template self._title_template = title_template
if self._title_template is not None:
self._title_template.hass = hass
self._notifiers = notifiers self._notifiers = notifiers
self._can_ack = can_ack self._can_ack = can_ack

View File

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
import logging import logging
from typing import Any from typing import Any
from typing_extensions import Generator
from homeassistant.components import ( from homeassistant.components import (
button, button,
climate, climate,
@ -19,6 +18,7 @@ from homeassistant.components import (
light, light,
media_player, media_player,
number, number,
remote,
timer, timer,
vacuum, vacuum,
valve, valve,
@ -439,6 +439,8 @@ class AlexaPowerController(AlexaCapability):
is_on = self.entity.state == fan.STATE_ON is_on = self.entity.state == fan.STATE_ON
elif self.entity.domain == humidifier.DOMAIN: elif self.entity.domain == humidifier.DOMAIN:
is_on = self.entity.state == humidifier.STATE_ON is_on = self.entity.state == humidifier.STATE_ON
elif self.entity.domain == remote.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
elif self.entity.domain == vacuum.DOMAIN: elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN: elif self.entity.domain == timer.DOMAIN:
@ -1436,6 +1438,12 @@ class AlexaModeController(AlexaCapability):
if mode in modes: if mode in modes:
return f"{humidifier.ATTR_MODE}.{mode}" return f"{humidifier.ATTR_MODE}.{mode}"
# Remote Activity
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
return f"{remote.ATTR_ACTIVITY}.{activity}"
# Water heater operation mode # Water heater operation mode
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get( operation_mode = self.entity.attributes.get(
@ -1550,6 +1558,24 @@ class AlexaModeController(AlexaCapability):
) )
return self._resource.serialize_capability_resources() return self._resource.serialize_capability_resources()
# Remote Resource
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
# Use the mode controller for a remote because the input controller
# only allows a preset of names as an input.
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
for activity in activities:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
)
# Remotes with a single activity completely break Alexa discovery, add a
# fake activity to the mode controller (see issue #53832).
if len(activities) == 1:
self._resource.add_mode(
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
)
return self._resource.serialize_capability_resources()
# Cover Position Resources # Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource( self._resource = AlexaModeResource(

View File

@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# AlexaModeController does not like a single mode for the fan preset or humidifier mode, # AlexaModeController does not like a single mode for the fan preset or humidifier mode,
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode # we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
PRESET_MODE_NA = "-" PRESET_MODE_NA = "-"
STORAGE_ACCESS_TOKEN = "access_token" STORAGE_ACCESS_TOKEN = "access_token"

View File

@ -2,12 +2,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Generator, Iterable
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from typing_extensions import Generator
from homeassistant.components import ( from homeassistant.components import (
alarm_control_panel, alarm_control_panel,
alert, alert,
@ -29,6 +27,7 @@ from homeassistant.components import (
lock, lock,
media_player, media_player,
number, number,
remote,
scene, scene,
script, script,
sensor, sensor,
@ -198,6 +197,10 @@ class DisplayCategory:
# Indicates a device that prints. # Indicates a device that prints.
PRINTER = "PRINTER" PRINTER = "PRINTER"
# Indicates a decive that support stateless events,
# such as remote switches and smart buttons.
REMOTE = "REMOTE"
# Indicates a network router. # Indicates a network router.
ROUTER = "ROUTER" ROUTER = "ROUTER"
@ -647,6 +650,27 @@ class FanCapabilities(AlexaEntity):
yield Alexa(self.entity) yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(remote.DOMAIN)
class RemoteCapabilities(AlexaEntity):
"""Class to represent Remote capabilities."""
def default_display_categories(self) -> list[str]:
"""Return the display categories for this entity."""
return [DisplayCategory.REMOTE]
def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(humidifier.DOMAIN) @ENTITY_ADAPTERS.register(humidifier.DOMAIN)
class HumidifierCapabilities(AlexaEntity): class HumidifierCapabilities(AlexaEntity):
"""Class to represent Humidifier capabilities.""" """Class to represent Humidifier capabilities."""

View File

@ -52,7 +52,6 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Initialize Alexa view.""" """Initialize Alexa view."""
super().__init__() super().__init__()
self.flash_briefings = flash_briefings self.flash_briefings = flash_briefings
template.attach(hass, self.flash_briefings)
@callback @callback
def get( def get(

View File

@ -21,6 +21,7 @@ from homeassistant.components import (
light, light,
media_player, media_player,
number, number,
remote,
timer, timer,
vacuum, vacuum,
valve, valve,
@ -185,6 +186,8 @@ async def async_api_turn_on(
service = fan.SERVICE_TURN_ON service = fan.SERVICE_TURN_ON
elif domain == humidifier.DOMAIN: elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_ON service = humidifier.SERVICE_TURN_ON
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_ON
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if ( if (
@ -234,6 +237,8 @@ async def async_api_turn_off(
service = climate.SERVICE_TURN_OFF service = climate.SERVICE_TURN_OFF
elif domain == fan.DOMAIN: elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_OFF service = fan.SERVICE_TURN_OFF
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_OFF
elif domain == humidifier.DOMAIN: elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_OFF service = humidifier.SERVICE_TURN_OFF
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
@ -1200,6 +1205,17 @@ async def async_api_set_mode(
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'" msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
# Remote Activity
elif instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = mode.split(".")[1]
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
if activity != PRESET_MODE_NA and activities and activity in activities:
service = remote.SERVICE_TURN_ON
data[remote.ATTR_ACTIVITY] = activity
else:
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg)
# Water heater operation mode # Water heater operation mode
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = mode.split(".")[1] operation_mode = mode.split(".")[1]
@ -1497,7 +1513,7 @@ async def async_api_adjust_range(
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION service = SERVICE_SET_COVER_POSITION
if not (current := entity.attributes.get(cover.ATTR_POSITION)): if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position" msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current)) position = response_value = min(100, max(0, range_delta + current))

View File

@ -5,5 +5,6 @@
"codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"], "codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"],
"dependencies": ["http"], "dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/alexa", "documentation": "https://www.home-assistant.io/integrations/alexa",
"integration_type": "system",
"iot_class": "cloud_push" "iot_class": "cloud_push"
} }

View File

@ -283,7 +283,7 @@ class AlexaPresetResource(AlexaCapabilityResource):
"""Implements Alexa PresetResources. """Implements Alexa PresetResources.
Use presetResources with RangeController to provide a set of Use presetResources with RangeController to provide a set of
friendlyNamesfor each RangeController preset. friendlyNames for each RangeController preset.
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
""" """

View File

@ -194,7 +194,7 @@ async def async_handle_message(
try: try:
if not enabled: if not enabled:
raise AlexaBridgeUnreachableError( raise AlexaBridgeUnreachableError( # noqa: TRY301
"Alexa API not enabled in Home Assistant configuration" "Alexa API not enabled in Home Assistant configuration"
) )

View File

@ -8,128 +8,23 @@ CONF_REGION: Final = "region_name"
CONF_ACCESS_KEY_ID: Final = "aws_access_key_id" CONF_ACCESS_KEY_ID: Final = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY: Final = "aws_secret_access_key" CONF_SECRET_ACCESS_KEY: Final = "aws_secret_access_key"
DEFAULT_REGION: Final = "us-east-1"
SUPPORTED_REGIONS: Final[list[str]] = [
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"ca-central-1",
"eu-west-1",
"eu-central-1",
"eu-west-2",
"eu-west-3",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-2",
"ap-northeast-1",
"ap-south-1",
"sa-east-1",
]
CONF_ENGINE: Final = "engine" CONF_ENGINE: Final = "engine"
CONF_VOICE: Final = "voice" CONF_VOICE: Final = "voice"
CONF_OUTPUT_FORMAT: Final = "output_format" CONF_OUTPUT_FORMAT: Final = "output_format"
CONF_SAMPLE_RATE: Final = "sample_rate" CONF_SAMPLE_RATE: Final = "sample_rate"
CONF_TEXT_TYPE: Final = "text_type" CONF_TEXT_TYPE: Final = "text_type"
SUPPORTED_VOICES: Final[list[str]] = [ SUPPORTED_OUTPUT_FORMATS: Final[set[str]] = {"mp3", "ogg_vorbis", "pcm"}
"Aditi", # Hindi
"Amy", # English (British)
"Aria", # English (New Zealand), Neural
"Arlet", # Catalan, Neural
"Arthur", # English, Neural
"Astrid", # Swedish
"Ayanda", # English (South African), Neural
"Bianca", # Italian
"Brian", # English (British)
"Camila", # Portuguese, Brazilian
"Carla", # Italian
"Carmen", # Romanian
"Celine", # French
"Chantal", # French Canadian
"Conchita", # Spanish (European)
"Cristiano", # Portuguese (European)
"Daniel", # German, Neural
"Dora", # Icelandic
"Elin", # Swedish, Neural
"Emma", # English
"Enrique", # Spanish (European)
"Ewa", # Polish
"Filiz", # Turkish
"Gabrielle", # French (Canadian)
"Geraint", # English Welsh
"Giorgio", # Italian
"Gwyneth", # Welsh
"Hala", # Arabic (Gulf), Neural
"Hannah", # German (Austrian), Neural
"Hans", # German
"Hiujin", # Chinese (Cantonese), Neural
"Ida", # Norwegian, Neural
"Ines", # Portuguese, European # codespell:ignore ines
"Ivy", # English
"Jacek", # Polish
"Jan", # Polish
"Joanna", # English
"Joey", # English
"Justin", # English
"Kajal", # English (Indian)/Hindi (Bilingual ), Neural
"Karl", # Icelandic
"Kendra", # English
"Kevin", # English, Neural
"Kimberly", # English
"Laura", # Dutch, Neural
"Lea", # French
"Liam", # Canadian French, Neural
"Liv", # Norwegian
"Lotte", # Dutch
"Lucia", # Spanish European
"Lupe", # Spanish US
"Mads", # Danish
"Maja", # Polish
"Marlene", # German
"Mathieu", # French
"Matthew", # English
"Maxim", # Russian
"Mia", # Spanish Mexican
"Miguel", # Spanish US
"Mizuki", # Japanese
"Naja", # Danish
"Nicole", # English Australian
"Ola", # Polish, Neural
"Olivia", # Female, Australian, Neural
"Penelope", # Spanish US
"Pedro", # Spanish US, Neural
"Raveena", # English, Indian
"Ricardo", # Portuguese (Brazilian)
"Ruben", # Dutch
"Russell", # English (Australian)
"Ruth", # English, Neural
"Salli", # English
"Seoyeon", # Korean
"Stephen", # English, Neural
"Suvi", # Finnish
"Takumi", # Japanese
"Tatyana", # Russian
"Vicki", # German
"Vitoria", # Portuguese, Brazilian
"Zeina", # Arabic
"Zhiyu", # Chinese
]
SUPPORTED_OUTPUT_FORMATS: Final[list[str]] = ["mp3", "ogg_vorbis", "pcm"] SUPPORTED_SAMPLE_RATES: Final[set[str]] = {"8000", "16000", "22050", "24000"}
SUPPORTED_ENGINES: Final[list[str]] = ["neural", "standard"] SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, set[str]]] = {
"mp3": {"8000", "16000", "22050", "24000"},
SUPPORTED_SAMPLE_RATES: Final[list[str]] = ["8000", "16000", "22050", "24000"] "ogg_vorbis": {"8000", "16000", "22050"},
"pcm": {"8000", "16000"},
SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, list[str]]] = {
"mp3": ["8000", "16000", "22050", "24000"],
"ogg_vorbis": ["8000", "16000", "22050"],
"pcm": ["8000", "16000"],
} }
SUPPORTED_TEXT_TYPES: Final[list[str]] = ["text", "ssml"] SUPPORTED_TEXT_TYPES: Final[set[str]] = {"text", "ssml"}
CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = { CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = {
"audio/mpeg": "mp3", "audio/mpeg": "mp3",
@ -137,6 +32,8 @@ CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = {
"audio/pcm": "pcm", "audio/pcm": "pcm",
} }
DEFAULT_REGION: Final = "us-east-1"
DEFAULT_ENGINE: Final = "standard" DEFAULT_ENGINE: Final = "standard"
DEFAULT_VOICE: Final = "Joanna" DEFAULT_VOICE: Final = "Joanna"
DEFAULT_OUTPUT_FORMAT: Final = "mp3" DEFAULT_OUTPUT_FORMAT: Final = "mp3"

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"], "loggers": ["boto3", "botocore", "s3transfer"],
"requirements": ["boto3==1.34.51"] "requirements": ["boto3==1.34.131"]
} }

View File

@ -16,6 +16,11 @@ from homeassistant.components.tts import (
) )
from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.generated.amazon_polly import (
SUPPORTED_ENGINES,
SUPPORTED_REGIONS,
SUPPORTED_VOICES,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -38,13 +43,10 @@ from .const import (
DEFAULT_SAMPLE_RATES, DEFAULT_SAMPLE_RATES,
DEFAULT_TEXT_TYPE, DEFAULT_TEXT_TYPE,
DEFAULT_VOICE, DEFAULT_VOICE,
SUPPORTED_ENGINES,
SUPPORTED_OUTPUT_FORMATS, SUPPORTED_OUTPUT_FORMATS,
SUPPORTED_REGIONS,
SUPPORTED_SAMPLE_RATES, SUPPORTED_SAMPLE_RATES,
SUPPORTED_SAMPLE_RATES_MAP, SUPPORTED_SAMPLE_RATES_MAP,
SUPPORTED_TEXT_TYPES, SUPPORTED_TEXT_TYPES,
SUPPORTED_VOICES,
) )
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)

View File

@ -71,6 +71,18 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
} }
class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
"""Sensor to show whether demand window is active."""
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
grid = self.coordinator.data["grid"]
if "demand_window" in grid:
return grid["demand_window"] # type: ignore[no-any-return]
return None
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
@ -83,6 +95,14 @@ async def async_setup_entry(
key="price_spike", key="price_spike",
name=f"{entry.title} - Price Spike", name=f"{entry.title} - Price Spike",
) )
async_add_entities( demand_window_description = BinarySensorEntityDescription(
[AmberPriceSpikeBinarySensor(coordinator, price_spike_description)] key="demand_window",
name=f"{entry.title} - Demand Window",
translation_key="demand_window",
)
async_add_entities(
[
AmberPriceSpikeBinarySensor(coordinator, price_spike_description),
AmberDemandWindowBinarySensor(coordinator, demand_window_description),
]
) )

View File

@ -111,6 +111,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
] ]
result["grid"]["renewables"] = round(general[0].renewables) result["grid"]["renewables"] = round(general[0].renewables)
result["grid"]["price_spike"] = general[0].spike_status.value result["grid"]["price_spike"] = general[0].spike_status.value
tariff_information = general[0].tariff_information
if tariff_information:
result["grid"]["demand_window"] = tariff_information.demand_window
controlled_load = [ controlled_load = [
interval for interval in current if is_controlled_load(interval) interval for interval in current if is_controlled_load(interval)

View File

@ -13,6 +13,14 @@
"renewables": { "renewables": {
"default": "mdi:solar-power" "default": "mdi:solar-power"
} }
},
"binary_sensor": {
"demand_window": {
"default": "mdi:meter-electric",
"state": {
"off": "mdi:meter-electric-outline"
}
}
} }
} }
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric", "documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["amberelectric"], "loggers": ["amberelectric"],
"requirements": ["amberelectric==1.1.0"] "requirements": ["amberelectric==1.1.1"]
} }

View File

@ -17,7 +17,6 @@ from homeassistant.const import (
) )
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
@ -25,7 +24,6 @@ import homeassistant.helpers.entity_registry as er
from .const import ( from .const import (
ATTR_LAST_DATA, ATTR_LAST_DATA,
CONF_APP_KEY, CONF_APP_KEY,
DOMAIN,
LOGGER, LOGGER,
TYPE_SOLARRADIATION, TYPE_SOLARRADIATION,
TYPE_SOLARRADIATION_LX, TYPE_SOLARRADIATION_LX,
@ -37,7 +35,6 @@ DATA_CONFIG = "config"
DEFAULT_SOCKET_MIN_RETRY = 15 DEFAULT_SOCKET_MIN_RETRY = 15
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
type AmbientStationConfigEntry = ConfigEntry[AmbientStation] type AmbientStationConfigEntry = ConfigEntry[AmbientStation]

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from aioambient import API from aioambient import API
from aioambient.errors import AmbientError from aioambient.errors import AmbientError
import voluptuous as vol import voluptuous as vol
@ -32,7 +34,9 @@ class AmbientStationFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors if errors else {}, errors=errors if errors else {},
) )
async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return await self._show_form() return await self._show_form()

View File

@ -8,6 +8,7 @@ from datetime import timedelta
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import aiohttp
from aiohttp import web from aiohttp import web
from amcrest import AmcrestError from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
@ -244,7 +245,9 @@ class AmcrestCam(Camera):
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
streaming_url = self._api.mjpeg_url(typeno=self._resolution) streaming_url = self._api.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get( stream_coro = websession.get(
streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT streaming_url,
auth=self._token,
timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT),
) )
return await async_aiohttp_proxy_web(self.hass, request, stream_coro) return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
@ -496,7 +499,7 @@ class AmcrestCam(Camera):
await getattr(self, f"_async_set_{func}")(value) await getattr(self, f"_async_set_{func}")(value)
new_value = await getattr(self, f"_async_get_{func}")() new_value = await getattr(self, f"_async_get_{func}")()
if new_value != value: if new_value != value:
raise AmcrestCommandFailed raise AmcrestCommandFailed # noqa: TRY301
except (AmcrestError, AmcrestCommandFailed) as error: except (AmcrestError, AmcrestCommandFailed) as error:
if tries == 1: if tries == 1:
log_update_error(_LOGGER, action, self.name, description, error) log_update_error(_LOGGER, action, self.name, description, error)

View File

@ -7,6 +7,6 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"], "loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.6.0"], "requirements": ["python-homeassistant-analytics==0.7.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -14,7 +14,6 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AndroidIPCamDataUpdateCoordinator from .coordinator import AndroidIPCamDataUpdateCoordinator
@ -27,9 +26,6 @@ PLATFORMS: list[Platform] = [
] ]
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Android IP Webcam from a config entry.""" """Set up Android IP Webcam from a config entry."""
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)

View File

@ -87,7 +87,7 @@ async def async_setup_entry(
"adb_command", "adb_command",
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent" SERVICE_LEARN_SENDEVENT, None, "learn_sendevent"
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_DOWNLOAD, SERVICE_DOWNLOAD,

View File

@ -101,7 +101,7 @@
}, },
"learn_sendevent": { "learn_sendevent": {
"name": "Learn sendevent", "name": "Learn sendevent",
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service." "description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of performing this action."
} }
}, },
"exceptions": { "exceptions": {

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova", "documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["anova_wifi"], "loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.14.0"] "requirements": ["anova-wifi==0.17.0"]
} }

View File

@ -0,0 +1,46 @@
"""The Anthropic integration."""
from __future__ import annotations
import anthropic
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, LOGGER
PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
"""Set up Anthropic from a config entry."""
client = anthropic.AsyncAnthropic(api_key=entry.data[CONF_API_KEY])
try:
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
except anthropic.AnthropicError as err:
raise ConfigEntryNotReady(err) from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Anthropic."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,210 @@
"""Config flow for Anthropic integration."""
from __future__ import annotations
import logging
from types import MappingProxyType
from typing import Any
import anthropic
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API
from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
TemplateSelector,
)
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_RECOMMENDED,
CONF_TEMPERATURE,
DOMAIN,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
)
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
)
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
client = anthropic.AsyncAnthropic(api_key=data[CONF_API_KEY])
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
await validate_input(self.hass, user_input)
except anthropic.APITimeoutError:
errors["base"] = "timeout_connect"
except anthropic.APIConnectionError:
errors["base"] = "cannot_connect"
except anthropic.APIStatusError as e:
if isinstance(e.body, dict):
errors["base"] = e.body.get("error", {}).get("type", "unknown")
else:
errors["base"] = "unknown"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Claude",
data=user_input,
options=RECOMMENDED_OPTIONS,
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors or None
)
@staticmethod
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
return AnthropicOptionsFlow(config_entry)
class AnthropicOptionsFlow(OptionsFlow):
"""Anthropic config flow options handler."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API)
return self.async_create_entry(title="", data=user_input)
# Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
}
suggested_values = options.copy()
if not suggested_values.get(CONF_PROMPT):
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
schema = self.add_suggested_values_to_schema(
vol.Schema(anthropic_config_option_schema(self.hass, options)),
suggested_values,
)
return self.async_show_form(
step_id="init",
data_schema=schema,
)
def anthropic_config_option_schema(
hass: HomeAssistant,
options: dict[str, Any] | MappingProxyType[str, Any],
) -> dict:
"""Return a schema for Anthropic completion options."""
hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label="No control",
value="none",
)
]
hass_apis.extend(
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
)
schema = {
vol.Optional(CONF_PROMPT): TemplateSelector(),
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector(
SelectSelectorConfig(options=hass_apis)
),
vol.Required(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
): bool,
}
if options.get(CONF_RECOMMENDED):
return schema
schema.update(
{
vol.Optional(
CONF_CHAT_MODEL,
default=RECOMMENDED_CHAT_MODEL,
): str,
vol.Optional(
CONF_MAX_TOKENS,
default=RECOMMENDED_MAX_TOKENS,
): int,
vol.Optional(
CONF_TEMPERATURE,
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
}
)
return schema

View File

@ -0,0 +1,15 @@
"""Constants for the Anthropic integration."""
import logging
DOMAIN = "anthropic"
LOGGER = logging.getLogger(__package__)
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0

View File

@ -0,0 +1,316 @@
"""Conversation support for Anthropic."""
from collections.abc import Callable
import json
from typing import Any, Literal, cast
import anthropic
from anthropic._types import NOT_GIVEN
from anthropic.types import (
Message,
MessageParam,
TextBlock,
TextBlockParam,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
)
import voluptuous as vol
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.components.conversation import trace
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.helpers import device_registry as dr, intent, llm, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import ulid
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_TEMPERATURE,
DOMAIN,
LOGGER,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AnthropicConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = AnthropicConversationEntity(config_entry)
async_add_entities([agent])
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _message_convert(
message: Message,
) -> MessageParam:
"""Convert from class to TypedDict."""
param_content: list[TextBlockParam | ToolUseBlockParam] = []
for message_content in message.content:
if isinstance(message_content, TextBlock):
param_content.append(TextBlockParam(type="text", text=message_content.text))
elif isinstance(message_content, ToolUseBlock):
param_content.append(
ToolUseBlockParam(
type="tool_use",
id=message_content.id,
name=message_content.name,
input=message_content.input,
)
)
return MessageParam(role=message.role, content=param_content)
class AnthropicConversationEntity(
conversation.ConversationEntity, conversation.AbstractConversationAgent
):
"""Anthropic conversation agent."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry: AnthropicConfigEntry) -> None:
"""Initialize the agent."""
self.entry = entry
self.history: dict[str, list[MessageParam]] = {}
self._attr_unique_id = entry.entry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
if self.entry.options.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
@property
def supported_languages(self) -> list[str] | Literal["*"]:
"""Return a list of supported languages."""
return MATCH_ALL
async def async_added_to_hass(self) -> None:
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
self.entry.async_on_unload(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def async_process(
self, user_input: conversation.ConversationInput
) -> conversation.ConversationResult:
"""Process a sentence."""
options = self.entry.options
intent_response = intent.IntentResponse(language=user_input.language)
llm_api: llm.APIInstance | None = None
tools: list[ToolParam] | None = None
user_name: str | None = None
llm_context = llm.LLMContext(
platform=DOMAIN,
context=user_input.context,
user_prompt=user_input.text,
language=user_input.language,
assistant=conversation.DOMAIN,
device_id=user_input.device_id,
)
if options.get(CONF_LLM_HASS_API):
try:
llm_api = await llm.async_get_api(
self.hass,
options[CONF_LLM_HASS_API],
llm_context,
)
except HomeAssistantError as err:
LOGGER.error("Error getting LLM API: %s", err)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Error preparing LLM API: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=user_input.conversation_id
)
tools = [
_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools
]
if user_input.conversation_id is None:
conversation_id = ulid.ulid_now()
messages = []
elif user_input.conversation_id in self.history:
conversation_id = user_input.conversation_id
messages = self.history[conversation_id]
else:
# Conversation IDs are ULIDs. We generate a new one if not provided.
# If an old OLID is passed in, we will generate a new one to indicate
# a new conversation was started. If the user picks their own, they
# want to track a conversation and we respect it.
try:
ulid.ulid_to_bytes(user_input.conversation_id)
conversation_id = ulid.ulid_now()
except ValueError:
conversation_id = user_input.conversation_id
messages = []
if (
user_input.context
and user_input.context.user_id
and (
user := await self.hass.auth.async_get_user(user_input.context.user_id)
)
):
user_name = user.name
try:
prompt_parts = [
template.Template(
llm.BASE_PROMPT
+ options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT),
self.hass,
).async_render(
{
"ha_name": self.hass.config.location_name,
"user_name": user_name,
"llm_context": llm_context,
},
parse_result=False,
)
]
except TemplateError as err:
LOGGER.error("Error rendering prompt: %s", err)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem with my template: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
if llm_api:
prompt_parts.append(llm_api.api_prompt)
prompt = "\n".join(prompt_parts)
# Create a copy of the variable because we attach it to the trace
messages = [*messages, MessageParam(role="user", content=user_input.text)]
LOGGER.debug("Prompt: %s", messages)
LOGGER.debug("Tools: %s", tools)
trace.async_conversation_trace_append(
trace.ConversationTraceEventType.AGENT_DETAIL,
{"system": prompt, "messages": messages},
)
client = self.entry.runtime_data
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
try:
response = await client.messages.create(
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
messages=messages,
tools=tools or NOT_GIVEN,
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
system=prompt,
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
)
except anthropic.AnthropicError as err:
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem talking to Anthropic: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
LOGGER.debug("Response %s", response)
messages.append(_message_convert(response))
if response.stop_reason != "tool_use" or not llm_api:
break
tool_results: list[ToolResultBlockParam] = []
for tool_call in response.content:
if isinstance(tool_call, TextBlock):
LOGGER.info(tool_call.text)
if not isinstance(tool_call, ToolUseBlock):
continue
tool_input = llm.ToolInput(
tool_name=tool_call.name,
tool_args=cast(dict[str, Any], tool_call.input),
)
LOGGER.debug(
"Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args
)
try:
tool_response = await llm_api.async_call_tool(tool_input)
except (HomeAssistantError, vol.Invalid) as e:
tool_response = {"error": type(e).__name__}
if str(e):
tool_response["error_text"] = str(e)
LOGGER.debug("Tool response: %s", tool_response)
tool_results.append(
ToolResultBlockParam(
type="tool_result",
tool_use_id=tool_call.id,
content=json.dumps(tool_response),
)
)
messages.append(MessageParam(role="user", content=tool_results))
self.history[conversation_id] = messages
for content in response.content:
if isinstance(content, TextBlock):
intent_response.async_set_speech(content.text)
break
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
async def _async_entry_update_listener(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)

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