Merge branch 'dev' into max-recursion

This commit is contained in:
Thomas55555
2024-09-17 16:04:55 +02:00
committed by GitHub
4103 changed files with 160525 additions and 72663 deletions

View File

@@ -14,6 +14,7 @@ core: &core
base_platforms: &base_platforms base_platforms: &base_platforms
- homeassistant/components/air_quality/** - homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/** - homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/**
- homeassistant/components/binary_sensor/** - homeassistant/components/binary_sensor/**
- homeassistant/components/button/** - homeassistant/components/button/**
- homeassistant/components/calendar/** - homeassistant/components/calendar/**
@@ -61,6 +62,7 @@ components: &components
- homeassistant/components/auth/** - homeassistant/components/auth/**
- homeassistant/components/automation/** - homeassistant/components/automation/**
- homeassistant/components/backup/** - homeassistant/components/backup/**
- homeassistant/components/blueprint/**
- homeassistant/components/bluetooth/** - homeassistant/components/bluetooth/**
- homeassistant/components/cloud/** - homeassistant/components/cloud/**
- homeassistant/components/config/** - homeassistant/components/config/**
@@ -124,9 +126,11 @@ tests: &tests
- tests/*.py - tests/*.py
- tests/auth/** - tests/auth/**
- tests/backports/** - tests/backports/**
- tests/components/diagnostics/**
- tests/components/history/** - tests/components/history/**
- tests/components/logbook/** - tests/components/logbook/**
- tests/components/recorder/** - tests/components/recorder/**
- tests/components/repairs/**
- tests/components/sensor/** - tests/components/sensor/**
- tests/hassfest/** - tests/hassfest/**
- tests/helpers/** - tests/helpers/**

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.1 uses: actions/setup-python@v5.2.0
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.4 uses: actions/upload-artifact@v4.4.0
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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -126,7 +126,7 @@ jobs:
env: env:
UV_PRERELEASE: allow UV_PRERELEASE: allow
run: | run: |
python3 -m pip install "$(grep '^uv' < requirements_test.txt)" python3 -m pip install "$(grep '^uv' < requirements.txt)"
uv pip install packaging tomli uv pip install packaging tomli
uv pip install . uv pip install .
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}" python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
@@ -197,7 +197,7 @@ jobs:
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 \
@@ -263,7 +263,7 @@ jobs:
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 \
@@ -316,6 +316,7 @@ jobs:
packages: write packages: write
id-token: write id-token: write
strategy: strategy:
fail-fast: false
matrix: matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
@@ -323,7 +324,7 @@ 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"
@@ -453,7 +454,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -482,3 +483,56 @@ jobs:
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
twine upload dist/* --skip-existing twine upload dist/* --skip-existing
hassfest-image:
name: Build and test hassfest image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
needs: ["init"]
if: github.repository_owner == 'home-assistant'
env:
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
load: true
tags: ${{ env.HASSFEST_IMAGE_TAG }}
- name: Run hassfest against core
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
push: true
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -37,10 +37,10 @@ on:
type: boolean 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.9" HA_SHORT_VERSION: "2024.10"
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
@@ -234,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -252,7 +252,7 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(grep '^uv' < requirements_test.txt)" pip install "$(grep '^uv' < requirements.txt)"
uv pip install "$(cat requirements_test.txt | grep pre-commit)" uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
@@ -279,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.1 uses: actions/setup-python@v5.2.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -319,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.1 uses: actions/setup-python@v5.2.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -359,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.1 uses: actions/setup-python@v5.2.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -429,17 +429,32 @@ jobs:
. venv/bin/activate . venv/bin/activate
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
lint-hadolint:
name: Check ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- info
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
strategy:
fail-fast: false
matrix:
file:
- Dockerfile
- Dockerfile.dev
- script/hassfest/docker/Dockerfile
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Register hadolint problem matcher - name: Register hadolint problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json" echo "::add-matcher::.github/workflows/matchers/hadolint.json"
- name: Check Dockerfile - name: Check ${{ matrix.file }}
uses: docker://hadolint/hadolint:v1.18.2 uses: docker://hadolint/hadolint:v2.12.0
with: with:
args: hadolint Dockerfile args: hadolint ${{ matrix.file }}
- name: Check Dockerfile.dev
uses: docker://hadolint/hadolint:v1.18.2
with:
args: hadolint Dockerfile.dev
base: base:
name: Prepare dependencies name: Prepare dependencies
@@ -454,14 +469,14 @@ 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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Generate partial uv restore key - name: Generate partial uv restore key
id: generate-uv-key id: generate-uv-key
run: | run: |
uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3) uv_version=$(cat requirements.txt | grep uv | cut -d '=' -f 3)
echo "version=${uv_version}" >> $GITHUB_OUTPUT echo "version=${uv_version}" >> $GITHUB_OUTPUT
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{ echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
@@ -510,12 +525,11 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(grep '^uv' < requirements_test.txt)" pip install "$(grep '^uv' < requirements.txt)"
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:
@@ -539,7 +553,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -572,7 +586,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -606,7 +620,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -624,7 +638,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pip-licenses --format=json --output-file=licenses.json pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses - name: Upload licenses
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: licenses name: licenses
path: licenses.json path: licenses.json
@@ -649,7 +663,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -696,7 +710,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -717,14 +731,14 @@ 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
@@ -741,7 +755,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -816,7 +830,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -834,7 +848,7 @@ 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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: pytest_buckets name: pytest_buckets
path: pytest_buckets.txt path: pytest_buckets.txt
@@ -880,7 +894,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -935,14 +949,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.4 uses: actions/upload-artifact@v4.4.0
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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@@ -1000,7 +1014,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1061,7 +1075,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.4 uses: actions/upload-artifact@v4.4.0
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 }}
@@ -1069,7 +1083,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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@@ -1126,7 +1140,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1188,7 +1202,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.4 uses: actions/upload-artifact@v4.4.0
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 }}
@@ -1196,7 +1210,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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@@ -1272,7 +1286,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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1330,14 +1344,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.4 uses: actions/upload-artifact@v4.4.0
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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml

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.15 uses: github/codeql-action/init@v3.26.7
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.25.15 uses: github/codeql-action/analyze@v3.26.7
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.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.1 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -46,7 +46,7 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(grep '^uv' < requirements_test.txt)" pip install "$(grep '^uv' < requirements.txt)"
uv pip install -r requirements.txt uv pip install -r requirements.txt
- name: Get information - name: Get information
@@ -82,14 +82,15 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
include-hidden-files: true
overwrite: true overwrite: true
- name: Upload requirements_diff - name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
@@ -101,7 +102,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.4 uses: actions/upload-artifact@v4.4.0
with: with:
name: requirements_all_wheels name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt path: ./requirements_all_wheels_*.txt
@@ -130,6 +131,12 @@ jobs:
with: with:
name: requirements_diff name: requirements_diff
- name: Adjust build env
run: |
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.07.1 uses: home-assistant/wheels@2024.07.1
with: with:
@@ -139,7 +146,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm" apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
skip-binary: aiohttp skip-binary: aiohttp;multidict;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt" requirements: "requirements.txt"
@@ -173,6 +180,18 @@ jobs:
with: with:
name: requirements_all_wheels name: requirements_all_wheels
- name: Adjust build env
run: |
if [ "${{ matrix.arch }}" = "i386" ]; then
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
# Do not pin numpy in wheels building
sed -i "/numpy/d" homeassistant/package_constraints.txt
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
- name: Split requirements all - name: Split requirements all
run: | run: |
# We split requirements all into multiple files. # We split requirements all into multiple files.
@@ -193,15 +212,6 @@ jobs:
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
- name: Adjust build env
run: |
if [ "${{ matrix.arch }}" = "i386" ]; then
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
# Do not pin numpy in wheels building
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (old cython) - name: Build wheels (old cython)
uses: home-assistant/wheels@2024.07.1 uses: home-assistant/wheels@2024.07.1
with: with:
@@ -211,7 +221,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" 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;pymicro-vad skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
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"
@@ -226,7 +236,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;pymicro-vad skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
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"
@@ -240,7 +250,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;pymicro-vad skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
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"
@@ -254,7 +264,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;pymicro-vad skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
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.5.6 rev: v0.6.5
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|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\.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

@@ -95,8 +95,7 @@ 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.assist_satellite.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.* homeassistant.components.asuswrt.*
homeassistant.components.autarco.* homeassistant.components.autarco.*
homeassistant.components.auth.* homeassistant.components.auth.*
@@ -112,6 +111,7 @@ homeassistant.components.bitcoin.*
homeassistant.components.blockchain.* homeassistant.components.blockchain.*
homeassistant.components.blue_current.* homeassistant.components.blue_current.*
homeassistant.components.blueprint.* homeassistant.components.blueprint.*
homeassistant.components.bluesound.*
homeassistant.components.bluetooth.* homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.* homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.* homeassistant.components.bluetooth_tracker.*
@@ -141,6 +141,7 @@ homeassistant.components.cpuspeed.*
homeassistant.components.crownstone.* homeassistant.components.crownstone.*
homeassistant.components.date.* homeassistant.components.date.*
homeassistant.components.datetime.* homeassistant.components.datetime.*
homeassistant.components.deako.*
homeassistant.components.deconz.* homeassistant.components.deconz.*
homeassistant.components.default_config.* homeassistant.components.default_config.*
homeassistant.components.demo.* homeassistant.components.demo.*
@@ -198,7 +199,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.*
@@ -208,6 +211,8 @@ homeassistant.components.glances.*
homeassistant.components.goalzero.* homeassistant.components.goalzero.*
homeassistant.components.google.* homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.* homeassistant.components.google_assistant_sdk.*
homeassistant.components.google_cloud.*
homeassistant.components.google_photos.*
homeassistant.components.google_sheets.* homeassistant.components.google_sheets.*
homeassistant.components.gpsd.* homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.* homeassistant.components.greeneye_monitor.*
@@ -278,6 +283,7 @@ homeassistant.components.lawn_mower.*
homeassistant.components.lcn.* homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.* homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.* homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.lidarr.* homeassistant.components.lidarr.*
homeassistant.components.lifx.* homeassistant.components.lifx.*
homeassistant.components.light.* homeassistant.components.light.*
@@ -294,7 +300,7 @@ homeassistant.components.london_underground.*
homeassistant.components.lookin.* homeassistant.components.lookin.*
homeassistant.components.luftdaten.* homeassistant.components.luftdaten.*
homeassistant.components.madvr.* homeassistant.components.madvr.*
homeassistant.components.mailbox.* homeassistant.components.manual.*
homeassistant.components.map.* homeassistant.components.map.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
homeassistant.components.matrix.* homeassistant.components.matrix.*
@@ -310,6 +316,7 @@ homeassistant.components.minecraft_server.*
homeassistant.components.mjpeg.* homeassistant.components.mjpeg.*
homeassistant.components.modbus.* homeassistant.components.modbus.*
homeassistant.components.modem_callerid.* homeassistant.components.modem_callerid.*
homeassistant.components.mold_indicator.*
homeassistant.components.monzo.* homeassistant.components.monzo.*
homeassistant.components.moon.* homeassistant.components.moon.*
homeassistant.components.mopeka.* homeassistant.components.mopeka.*
@@ -336,6 +343,7 @@ homeassistant.components.nut.*
homeassistant.components.onboarding.* homeassistant.components.onboarding.*
homeassistant.components.oncue.* homeassistant.components.oncue.*
homeassistant.components.onewire.* homeassistant.components.onewire.*
homeassistant.components.onkyo.*
homeassistant.components.open_meteo.* homeassistant.components.open_meteo.*
homeassistant.components.openexchangerates.* homeassistant.components.openexchangerates.*
homeassistant.components.opensky.* homeassistant.components.opensky.*
@@ -395,6 +403,7 @@ homeassistant.components.select.*
homeassistant.components.sensibo.* homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.* homeassistant.components.sensirion_ble.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
homeassistant.components.sensoterra.*
homeassistant.components.senz.* homeassistant.components.senz.*
homeassistant.components.sfr_box.* homeassistant.components.sfr_box.*
homeassistant.components.shelly.* homeassistant.components.shelly.*
@@ -406,10 +415,13 @@ homeassistant.components.skybell.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.sleepiq.* homeassistant.components.sleepiq.*
homeassistant.components.smhi.* homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.snooz.* homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.* homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.* homeassistant.components.speedtestdotnet.*
homeassistant.components.sql.* homeassistant.components.sql.*
homeassistant.components.squeezebox.*
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
homeassistant.components.starlink.* homeassistant.components.starlink.*
homeassistant.components.statistics.* homeassistant.components.statistics.*
@@ -469,6 +481,7 @@ homeassistant.components.update.*
homeassistant.components.uptime.* homeassistant.components.uptime.*
homeassistant.components.uptimerobot.* homeassistant.components.uptimerobot.*
homeassistant.components.usb.* homeassistant.components.usb.*
homeassistant.components.uvc.*
homeassistant.components.vacuum.* homeassistant.components.vacuum.*
homeassistant.components.vallox.* homeassistant.components.vallox.*
homeassistant.components.valve.* homeassistant.components.valve.*

View File

@@ -48,6 +48,7 @@ build.json @home-assistant/supervisor
/tests/components/adax/ @danielhiversen /tests/components/adax/ @danielhiversen
/homeassistant/components/adguard/ @frenck /homeassistant/components/adguard/ @frenck
/tests/components/adguard/ @frenck /tests/components/adguard/ @frenck
/homeassistant/components/ads/ @mrpasztoradam
/homeassistant/components/advantage_air/ @Bre77 /homeassistant/components/advantage_air/ @Bre77
/tests/components/advantage_air/ @Bre77 /tests/components/advantage_air/ @Bre77
/homeassistant/components/aemet/ @Noltari /homeassistant/components/aemet/ @Noltari
@@ -143,6 +144,8 @@ build.json @home-assistant/supervisor
/tests/components/aseko_pool_live/ @milanmeu /tests/components/aseko_pool_live/ @milanmeu
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam /homeassistant/components/assist_pipeline/ @balloob @synesthesiam
/tests/components/assist_pipeline/ @balloob @synesthesiam /tests/components/assist_pipeline/ @balloob @synesthesiam
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 /homeassistant/components/asuswrt/ @kennedyshead @ollo69
/tests/components/asuswrt/ @kennedyshead @ollo69 /tests/components/asuswrt/ @kennedyshead @ollo69
/homeassistant/components/atag/ @MatsNL /homeassistant/components/atag/ @MatsNL
@@ -228,14 +231,16 @@ build.json @home-assistant/supervisor
/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
/homeassistant/components/bthome/ @Ernst79 /homeassistant/components/bthome/ @Ernst79 @thecode
/tests/components/bthome/ @Ernst79 /tests/components/bthome/ @Ernst79 @thecode
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221 /homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
/tests/components/buienradar/ @mjj4791 @ties @Robbie1221 /tests/components/buienradar/ @mjj4791 @ties @Robbie1221
/homeassistant/components/button/ @home-assistant/core /homeassistant/components/button/ @home-assistant/core
/tests/components/button/ @home-assistant/core /tests/components/button/ @home-assistant/core
/homeassistant/components/calendar/ @home-assistant/core /homeassistant/components/calendar/ @home-assistant/core
/tests/components/calendar/ @home-assistant/core /tests/components/calendar/ @home-assistant/core
/homeassistant/components/cambridge_audio/ @noahhusby
/tests/components/cambridge_audio/ @noahhusby
/homeassistant/components/camera/ @home-assistant/core /homeassistant/components/camera/ @home-assistant/core
/tests/components/camera/ @home-assistant/core /tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery /homeassistant/components/cast/ @emontnemery
@@ -294,6 +299,8 @@ build.json @home-assistant/supervisor
/tests/components/date/ @home-assistant/core /tests/components/date/ @home-assistant/core
/homeassistant/components/datetime/ @home-assistant/core /homeassistant/components/datetime/ @home-assistant/core
/tests/components/datetime/ @home-assistant/core /tests/components/datetime/ @home-assistant/core
/homeassistant/components/deako/ @sebirdman @balake @deakolights
/tests/components/deako/ @sebirdman @balake @deakolights
/homeassistant/components/debugpy/ @frenck /homeassistant/components/debugpy/ @frenck
/tests/components/debugpy/ @frenck /tests/components/debugpy/ @frenck
/homeassistant/components/deconz/ @Kane610 /homeassistant/components/deconz/ @Kane610
@@ -349,10 +356,12 @@ 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/duke_energy/ @hunterjm
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
@@ -433,6 +442,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
@@ -498,6 +508,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
@@ -544,11 +556,14 @@ build.json @home-assistant/supervisor
/tests/components/google_assistant/ @home-assistant/cloud /tests/components/google_assistant/ @home-assistant/cloud
/homeassistant/components/google_assistant_sdk/ @tronikos /homeassistant/components/google_assistant_sdk/ @tronikos
/tests/components/google_assistant_sdk/ @tronikos /tests/components/google_assistant_sdk/ @tronikos
/homeassistant/components/google_cloud/ @lufton /homeassistant/components/google_cloud/ @lufton @tronikos
/tests/components/google_cloud/ @lufton @tronikos
/homeassistant/components/google_generative_ai_conversation/ @tronikos /homeassistant/components/google_generative_ai_conversation/ @tronikos
/tests/components/google_generative_ai_conversation/ @tronikos /tests/components/google_generative_ai_conversation/ @tronikos
/homeassistant/components/google_mail/ @tkdrob /homeassistant/components/google_mail/ @tkdrob
/tests/components/google_mail/ @tkdrob /tests/components/google_mail/ @tkdrob
/homeassistant/components/google_photos/ @allenporter
/tests/components/google_photos/ @allenporter
/homeassistant/components/google_sheets/ @tkdrob /homeassistant/components/google_sheets/ @tkdrob
/tests/components/google_sheets/ @tkdrob /tests/components/google_sheets/ @tkdrob
/homeassistant/components/google_tasks/ @allenporter /homeassistant/components/google_tasks/ @allenporter
@@ -626,6 +641,8 @@ build.json @home-assistant/supervisor
/tests/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer /homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman @mkmer /tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/http/ @home-assistant/core /homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core /tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle /homeassistant/components/huawei_lte/ @scop @fphammerle
@@ -704,8 +721,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 /homeassistant/components/iotty/ @pburgio @shapournemati-iotty
/tests/components/iotty/ @pburgio /tests/components/iotty/ @pburgio @shapournemati-iotty
/homeassistant/components/iperf3/ @rohankapoorcom /homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes /homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes /tests/components/ipma/ @dgomes
@@ -718,6 +735,8 @@ build.json @home-assistant/supervisor
/tests/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/iskra/ @iskramis
/tests/components/iskra/ @iskramis
/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 /homeassistant/components/israel_rail/ @shaiu
@@ -794,8 +813,12 @@ build.json @home-assistant/supervisor
/tests/components/leaone/ @bdraco /tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco /homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco
/homeassistant/components/lektrico/ @lektrico
/tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98 /tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi /homeassistant/components/lifx/ @Djelibeybi
@@ -825,8 +848,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
@@ -842,8 +863,8 @@ build.json @home-assistant/supervisor
/tests/components/lupusec/ @majuss @suaveolent /tests/components/lupusec/ @majuss @suaveolent
/homeassistant/components/lutron/ @cdheiser @wilburCForce /homeassistant/components/lutron/ @cdheiser @wilburCForce
/tests/components/lutron/ @cdheiser @wilburCForce /tests/components/lutron/ @cdheiser @wilburCForce
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151 /homeassistant/components/lutron_caseta/ @swails @danaues @eclair4151
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151 /tests/components/lutron_caseta/ @swails @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001 /homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea /homeassistant/components/madvr/ @iloveicedgreentea
@@ -906,6 +927,8 @@ build.json @home-assistant/supervisor
/tests/components/modern_forms/ @wonderslug /tests/components/modern_forms/ @wonderslug
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n /homeassistant/components/moehlenhoff_alpha2/ @j-a-n
/tests/components/moehlenhoff_alpha2/ @j-a-n /tests/components/moehlenhoff_alpha2/ @j-a-n
/homeassistant/components/monarch_money/ @jeeftor
/tests/components/monarch_money/ @jeeftor
/homeassistant/components/monoprice/ @etsinko @OnFreund /homeassistant/components/monoprice/ @etsinko @OnFreund
/tests/components/monoprice/ @etsinko @OnFreund /tests/components/monoprice/ @etsinko @OnFreund
/homeassistant/components/monzo/ @jakemartin-icl /homeassistant/components/monzo/ @jakemartin-icl
@@ -969,6 +992,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
@@ -1272,6 +1297,8 @@ build.json @home-assistant/supervisor
/tests/components/sensorpro/ @bdraco /tests/components/sensorpro/ @bdraco
/homeassistant/components/sensorpush/ @bdraco /homeassistant/components/sensorpush/ @bdraco
/tests/components/sensorpush/ @bdraco /tests/components/sensorpush/ @bdraco
/homeassistant/components/sensoterra/ @markruys
/tests/components/sensoterra/ @markruys
/homeassistant/components/sentry/ @dcramer @frenck /homeassistant/components/sentry/ @dcramer @frenck
/tests/components/sentry/ @dcramer @frenck /tests/components/sentry/ @dcramer @frenck
/homeassistant/components/senz/ @milanmeu /homeassistant/components/senz/ @milanmeu
@@ -1326,6 +1353,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
@@ -1407,8 +1436,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland /homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland /tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode /homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/switchmate/ @danielhiversen @qiz-li
@@ -1488,6 +1517,8 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp /tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek /homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek
/homeassistant/components/touchline_sl/ @jnsgruk
/tests/components/touchline_sl/ @jnsgruk
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696 /homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @bdraco @sdb9696 /tests/components/tplink/ @rytilahti @bdraco @sdb9696
/homeassistant/components/tplink_omada/ @MarkGodwin /homeassistant/components/tplink_omada/ @MarkGodwin
@@ -1512,6 +1543,8 @@ build.json @home-assistant/supervisor
/tests/components/transmission/ @engrbm87 @JPHutchins /tests/components/transmission/ @engrbm87 @JPHutchins
/homeassistant/components/trend/ @jpbede /homeassistant/components/trend/ @jpbede
/tests/components/trend/ @jpbede /tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey
/homeassistant/components/tts/ @home-assistant/core /homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core /tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck /homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
@@ -1616,6 +1649,8 @@ build.json @home-assistant/supervisor
/tests/components/webostv/ @thecode /tests/components/webostv/ @thecode
/homeassistant/components/websocket_api/ @home-assistant/core /homeassistant/components/websocket_api/ @home-assistant/core
/tests/components/websocket_api/ @home-assistant/core /tests/components/websocket_api/ @home-assistant/core
/homeassistant/components/weheat/ @jesperraemaekers
/tests/components/weheat/ @jesperraemaekers
/homeassistant/components/wemo/ @esev /homeassistant/components/wemo/ @esev
/tests/components/wemo/ @esev /tests/components/wemo/ @esev
/homeassistant/components/whirlpool/ @abmantis @mkmer /homeassistant/components/whirlpool/ @abmantis @mkmer
@@ -1633,6 +1668,8 @@ build.json @home-assistant/supervisor
/tests/components/wiz/ @sbidy /tests/components/wiz/ @sbidy
/homeassistant/components/wled/ @frenck /homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck /tests/components/wled/ @frenck
/homeassistant/components/wmspro/ @mback2k
/tests/components/wmspro/ @mback2k
/homeassistant/components/wolflink/ @adamkrol93 @mtielen /homeassistant/components/wolflink/ @adamkrol93 @mtielen
/tests/components/wolflink/ @adamkrol93 @mtielen /tests/components/wolflink/ @adamkrol93 @mtielen
/homeassistant/components/workday/ @fabaff @gjohansson-ST /homeassistant/components/workday/ @fabaff @gjohansson-ST
@@ -1653,6 +1690,8 @@ build.json @home-assistant/supervisor
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG /tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
/homeassistant/components/xiaomi_tv/ @simse /homeassistant/components/xiaomi_tv/ @simse
/homeassistant/components/xmpp/ @fabaff @flowolf /homeassistant/components/xmpp/ @fabaff @flowolf
/homeassistant/components/yale/ @bdraco
/tests/components/yale/ @bdraco
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST /homeassistant/components/yale_smart_alarm/ @gjohansson-ST
/tests/components/yale_smart_alarm/ @gjohansson-ST /tests/components/yale_smart_alarm/ @gjohansson-ST
/homeassistant/components/yalexs_ble/ @bdraco /homeassistant/components/yalexs_ble/ @bdraco

View File

@@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU ARG QEMU_CPU
# Install uv # Install uv
RUN pip3 install uv==0.2.27 RUN pip3 install uv==0.4.9
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

@@ -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

@@ -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

@@ -586,10 +586,10 @@ async 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,
@@ -616,10 +616,9 @@ async def async_enable_logging(
_create_log_file, err_log_path, log_rotate_days _create_log_file, err_log_path, log_rotate_days
) )
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)

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

@@ -9,6 +9,7 @@
"google_generative_ai_conversation", "google_generative_ai_conversation",
"google_mail", "google_mail",
"google_maps", "google_maps",
"google_photos",
"google_pubsub", "google_pubsub",
"google_sheets", "google_sheets",
"google_tasks", "google_tasks",

View File

@@ -1,5 +1,5 @@
{ {
"domain": "lg", "domain": "lg",
"name": "LG", "name": "LG",
"integrations": ["lg_netcast", "lg_soundbar", "webostv"] "integrations": ["lg_netcast", "lg_thinq", "lg_soundbar", "webostv"]
} }

View File

@@ -0,0 +1,5 @@
{
"domain": "roth",
"name": "Roth",
"integrations": ["touchline", "touchline_sl"]
}

View File

@@ -1,5 +1,11 @@
{ {
"domain": "yale", "domain": "yale",
"name": "Yale", "name": "Yale",
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"] "integrations": [
"august",
"yale_smart_alarm",
"yalexs_ble",
"yale_home",
"yale"
]
} }

View File

@@ -6,52 +6,3 @@ Component design guidelines:
format "<DOMAIN>.<OBJECT_ID>". format "<DOMAIN>.<OBJECT_ID>".
- Each component should publish services only under its own domain. - Each component should publish services only under its own domain.
""" """
from __future__ import annotations
import logging
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers.frame import report
from homeassistant.helpers.group import expand_entity_ids
_LOGGER = logging.getLogger(__name__)
def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
"""Load up the module to call the is_on method.
If there is no entity id given we will check all.
"""
report(
(
"uses homeassistant.components.is_on."
" This is deprecated and will stop working in Home Assistant 2024.9, it"
" should be updated to use the function of the platform directly."
),
error_if_core=True,
)
if entity_id:
entity_ids = expand_entity_ids(hass, [entity_id])
else:
entity_ids = hass.states.entity_ids()
for ent_id in entity_ids:
domain = split_entity_id(ent_id)[0]
try:
component = getattr(hass.components, domain)
except ImportError:
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
continue
if not hasattr(component, "is_on"):
_LOGGER.warning("Integration %s has no is_on method", domain)
continue
if component.is_on(ent_id):
return True
return False

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import cast from typing import cast
from jaraco.abode.devices.sensor import BinarySensor from jaraco.abode.devices.binary_sensor import BinarySensor
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,

View File

@@ -7,8 +7,14 @@
} }
}, },
"services": { "services": {
"capture_image": "mdi:camera", "capture_image": {
"change_setting": "mdi:cog", "service": "mdi:camera"
"trigger_automation": "mdi:play" },
"change_setting": {
"service": "mdi:cog"
},
"trigger_automation": {
"service": "mdi:play"
}
} }
} }

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.2.1"] "requirements": ["jaraco.abode==6.2.0"]
} }

View File

@@ -18,6 +18,7 @@ from homeassistant.const import (
UV_INDEX, UV_INDEX,
UnitOfIrradiance, UnitOfIrradiance,
UnitOfLength, UnitOfLength,
UnitOfPressure,
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
UnitOfTime, UnitOfTime,
@@ -279,6 +280,15 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="realfeel_temperature_shade", translation_key="realfeel_temperature_shade",
), ),
AccuWeatherSensorDescription(
key="RelativeHumidity",
device_class=SensorDeviceClass.HUMIDITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
translation_key="humidity",
),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Precipitation", key="Precipitation",
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
@@ -288,6 +298,16 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
attr_fn=lambda data: {"type": data["PrecipitationType"]}, attr_fn=lambda data: {"type": data["PrecipitationType"]},
translation_key="precipitation", translation_key="precipitation",
), ),
AccuWeatherSensorDescription(
key="Pressure",
device_class=SensorDeviceClass.PRESSURE,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
native_unit_of_measurement=UnitOfPressure.HPA,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="pressure",
),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="PressureTendency", key="PressureTendency",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
@@ -295,9 +315,19 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(), value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
translation_key="pressure_tendency", translation_key="pressure_tendency",
), ),
AccuWeatherSensorDescription(
key="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="temperature",
),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="UVIndex", key="UVIndex",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]}, attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
@@ -324,6 +354,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Wind", key="Wind",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),

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

@@ -3,6 +3,7 @@
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from .hub import PulseHub from .hub import PulseHub
@@ -17,6 +18,9 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: AcmedaConfigEntry hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool: ) -> bool:
"""Set up Rollease Acmeda Automate hub from a config entry.""" """Set up Rollease Acmeda Automate hub from a config entry."""
await _migrate_unique_ids(hass, config_entry)
hub = PulseHub(hass, config_entry) hub = PulseHub(hass, config_entry)
if not await hub.async_setup(): if not await hub.async_setup():
@@ -28,6 +32,19 @@ async def async_setup_entry(
return True return True
async def _migrate_unique_ids(hass: HomeAssistant, entry: AcmedaConfigEntry) -> None:
"""Migrate pre-config flow unique ids."""
entity_registry = er.async_get(hass)
registry_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
for reg_entry in registry_entries:
if isinstance(reg_entry.unique_id, int): # type: ignore[unreachable]
entity_registry.async_update_entity( # type: ignore[unreachable]
reg_entry.entity_id, new_unique_id=str(reg_entry.unique_id)
)
async def async_unload_entry( async def async_unload_entry(
hass: HomeAssistant, config_entry: AcmedaConfigEntry hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool: ) -> bool:

View File

@@ -14,8 +14,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AcmedaConfigEntry from . import AcmedaConfigEntry
from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE from .const import ACMEDA_HUB_UPDATE
from .entity import AcmedaEntity
from .helpers import async_add_acmeda_entities from .helpers import async_add_acmeda_entities
@@ -44,7 +44,7 @@ async def async_setup_entry(
) )
class AcmedaCover(AcmedaBase, CoverEntity): class AcmedaCover(AcmedaEntity, CoverEntity):
"""Representation of an Acmeda cover device.""" """Representation of an Acmeda cover device."""
_attr_name = None _attr_name = None

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER
class AcmedaBase(entity.Entity): class AcmedaEntity(entity.Entity):
"""Base representation of an Acmeda roller.""" """Base representation of an Acmeda roller."""
_attr_should_poll = False _attr_should_poll = False
@@ -67,7 +67,7 @@ class AcmedaBase(entity.Entity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return the unique ID of this roller.""" """Return the unique ID of this roller."""
return self.roller.id # type: ignore[no-any-return] return str(self.roller.id)
@property @property
def device_id(self) -> str: def device_id(self) -> str:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda", "documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiopulse"], "loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.4"] "requirements": ["aiopulse==0.4.6"]
} }

View File

@@ -9,8 +9,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AcmedaConfigEntry from . import AcmedaConfigEntry
from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE from .const import ACMEDA_HUB_UPDATE
from .entity import AcmedaEntity
from .helpers import async_add_acmeda_entities from .helpers import async_add_acmeda_entities
@@ -39,7 +39,7 @@ async def async_setup_entry(
) )
class AcmedaBattery(AcmedaBase, SensorEntity): class AcmedaBattery(AcmedaEntity, SensorEntity):
"""Representation of an Acmeda cover sensor.""" """Representation of an Acmeda cover sensor."""
_attr_device_class = SensorDeviceClass.BATTERY _attr_device_class = SensorDeviceClass.BATTERY

View File

@@ -9,7 +9,7 @@ from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, DOMAIN as DEVICE_TRACKER_DOMAIN,
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
DeviceScanner, DeviceScanner,
) )
@@ -36,7 +36,7 @@ def get_scanner(
hass: HomeAssistant, config: ConfigType hass: HomeAssistant, config: ConfigType
) -> ActiontecDeviceScanner | None: ) -> ActiontecDeviceScanner | None:
"""Validate the configuration and return an Actiontec scanner.""" """Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN]) scanner = ActiontecDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@@ -51,7 +51,6 @@ class ActiontecDeviceScanner(DeviceScanner):
self.last_results: list[Device] = [] self.last_results: list[Device] = []
data = self.get_actiontec_data() data = self.get_actiontec_data()
self.success_init = data is not None self.success_init = data is not None
_LOGGER.info("Scanner initialized")
def scan_devices(self) -> list[str]: def scan_devices(self) -> list[str]:
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@@ -70,7 +69,7 @@ class ActiontecDeviceScanner(DeviceScanner):
Return boolean if scanning successful. Return boolean if scanning successful.
""" """
_LOGGER.info("Scanning") _LOGGER.debug("Scanning")
if not self.success_init: if not self.success_init:
return False return False
@@ -79,7 +78,7 @@ class ActiontecDeviceScanner(DeviceScanner):
self.last_results = [ self.last_results = [
device for device in actiontec_data if device.timevalid > -60 device for device in actiontec_data if device.timevalid > -60
] ]
_LOGGER.info("Scan successful") _LOGGER.debug("Scan successful")
return True return True
def get_actiontec_data(self) -> list[Device] | None: def get_actiontec_data(self) -> list[Device] | None:

View File

@@ -130,7 +130,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
async_get_clientsession(self.hass), account_id, password async_get_clientsession(self.hass), account_id, password
) )
if token is None: if token is None:
_LOGGER.info("Adax: Failed to login to retrieve token") _LOGGER.debug("Adax: Failed to login to retrieve token")
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
return self.async_show_form( return self.async_show_form(
step_id="cloud", step_id="cloud",

View File

@@ -66,10 +66,20 @@
} }
}, },
"services": { "services": {
"add_url": "mdi:link-plus", "add_url": {
"remove_url": "mdi:link-off", "service": "mdi:link-plus"
"enable_url": "mdi:link-variant", },
"disable_url": "mdi:link-variant-off", "remove_url": {
"refresh": "mdi:refresh" "service": "mdi:link-off"
},
"enable_url": {
"service": "mdi:link-variant"
},
"disable_url": {
"service": "mdi:link-variant-off"
},
"refresh": {
"service": "mdi:refresh"
}
} }
} }

View File

@@ -1,12 +1,6 @@
"""Support for Automation Device Specification (ADS).""" """Support for Automation Device Specification (ADS)."""
import asyncio
from asyncio import timeout
from collections import namedtuple
import ctypes
import logging import logging
import struct
import threading
import pyads import pyads
import voluptuous as vol import voluptuous as vol
@@ -19,42 +13,38 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import CONF_ADS_VAR, DATA_ADS, DOMAIN, AdsType
from .hub import AdsHub
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_ADS = "data_ads"
# Supported Types
ADSTYPE_BOOL = "bool"
ADSTYPE_BYTE = "byte"
ADSTYPE_DINT = "dint"
ADSTYPE_INT = "int"
ADSTYPE_UDINT = "udint"
ADSTYPE_UINT = "uint"
ADS_TYPEMAP = { ADS_TYPEMAP = {
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, AdsType.BOOL: pyads.PLCTYPE_BOOL,
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, AdsType.BYTE: pyads.PLCTYPE_BYTE,
ADSTYPE_DINT: pyads.PLCTYPE_DINT, AdsType.INT: pyads.PLCTYPE_INT,
ADSTYPE_INT: pyads.PLCTYPE_INT, AdsType.UINT: pyads.PLCTYPE_UINT,
ADSTYPE_UDINT: pyads.PLCTYPE_UDINT, AdsType.SINT: pyads.PLCTYPE_SINT,
ADSTYPE_UINT: pyads.PLCTYPE_UINT, AdsType.USINT: pyads.PLCTYPE_USINT,
AdsType.DINT: pyads.PLCTYPE_DINT,
AdsType.UDINT: pyads.PLCTYPE_UDINT,
AdsType.WORD: pyads.PLCTYPE_WORD,
AdsType.DWORD: pyads.PLCTYPE_DWORD,
AdsType.REAL: pyads.PLCTYPE_REAL,
AdsType.LREAL: pyads.PLCTYPE_LREAL,
AdsType.STRING: pyads.PLCTYPE_STRING,
AdsType.TIME: pyads.PLCTYPE_TIME,
AdsType.DATE: pyads.PLCTYPE_DATE,
AdsType.DATE_AND_TIME: pyads.PLCTYPE_DT,
AdsType.TOD: pyads.PLCTYPE_TOD,
} }
CONF_ADS_FACTOR = "factor" CONF_ADS_FACTOR = "factor"
CONF_ADS_TYPE = "adstype" CONF_ADS_TYPE = "adstype"
CONF_ADS_VALUE = "value" CONF_ADS_VALUE = "value"
CONF_ADS_VAR = "adsvar"
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
CONF_ADS_VAR_POSITION = "adsvar_position"
STATE_KEY_STATE = "state"
STATE_KEY_BRIGHTNESS = "brightness"
STATE_KEY_POSITION = "position"
DOMAIN = "ads"
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name" SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
@@ -73,16 +63,7 @@ CONFIG_SCHEMA = vol.Schema(
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema( SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
{ {
vol.Required(CONF_ADS_TYPE): vol.In( vol.Required(CONF_ADS_TYPE): vol.Coerce(AdsType),
[
ADSTYPE_INT,
ADSTYPE_UINT,
ADSTYPE_BYTE,
ADSTYPE_BOOL,
ADSTYPE_DINT,
ADSTYPE_UDINT,
]
),
vol.Required(CONF_ADS_VALUE): vol.Coerce(int), vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
vol.Required(CONF_ADS_VAR): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
} }
@@ -116,9 +97,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
def handle_write_data_by_name(call: ServiceCall) -> None: def handle_write_data_by_name(call: ServiceCall) -> None:
"""Write a value to the connected ADS device.""" """Write a value to the connected ADS device."""
ads_var = call.data[CONF_ADS_VAR] ads_var: str = call.data[CONF_ADS_VAR]
ads_type = call.data[CONF_ADS_TYPE] ads_type: AdsType = call.data[CONF_ADS_TYPE]
value = call.data[CONF_ADS_VALUE] value: int = call.data[CONF_ADS_VALUE]
try: try:
ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type]) ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type])
@@ -133,181 +114,3 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
) )
return True return True
# Tuple to hold data needed for notification
NotificationItem = namedtuple( # noqa: PYI024
"NotificationItem", "hnotify huser name plc_datatype callback"
)
class AdsHub:
"""Representation of an ADS connection."""
def __init__(self, ads_client):
"""Initialize the ADS hub."""
self._client = ads_client
self._client.open()
# All ADS devices are registered here
self._devices = []
self._notification_items = {}
self._lock = threading.Lock()
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
_LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values():
_LOGGER.debug(
"Deleting device notification %d, %d",
notification_item.hnotify,
notification_item.huser,
)
try:
self._client.del_device_notification(
notification_item.hnotify, notification_item.huser
)
except pyads.ADSError as err:
_LOGGER.error(err)
try:
self._client.close()
except pyads.ADSError as err:
_LOGGER.error(err)
def register_device(self, device):
"""Register a new device."""
self._devices.append(device)
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
with self._lock:
try:
return self._client.write_by_name(name, value, plc_datatype)
except pyads.ADSError as err:
_LOGGER.error("Error writing %s: %s", name, err)
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
with self._lock:
try:
return self._client.read_by_name(name, plc_datatype)
except pyads.ADSError as err:
_LOGGER.error("Error reading %s: %s", name, err)
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
try:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback
)
except pyads.ADSError as err:
_LOGGER.error("Error subscribing to %s: %s", name, err)
else:
hnotify = int(hnotify)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback
)
_LOGGER.debug(
"Added device notification %d for variable %s", hnotify, name
)
def _device_notification_callback(self, notification, name):
"""Handle device notifications."""
contents = notification.contents
hnotify = int(contents.hNotification)
_LOGGER.debug("Received notification %d", hnotify)
# get dynamically sized data array
data_size = contents.cbSampleSize
data = (ctypes.c_ubyte * data_size).from_address(
ctypes.addressof(contents)
+ pyads.structs.SAdsNotificationHeader.data.offset
)
try:
with self._lock:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.error("Unknown device notification handle: %d", hnotify)
return
# Parse data to desired datatype
if notification_item.plc_datatype == pyads.PLCTYPE_BOOL:
value = bool(struct.unpack("<?", bytearray(data))[0])
elif notification_item.plc_datatype == pyads.PLCTYPE_INT:
value = struct.unpack("<h", bytearray(data))[0]
elif notification_item.plc_datatype == pyads.PLCTYPE_BYTE:
value = struct.unpack("<B", bytearray(data))[0]
elif notification_item.plc_datatype == pyads.PLCTYPE_UINT:
value = struct.unpack("<H", bytearray(data))[0]
elif notification_item.plc_datatype == pyads.PLCTYPE_DINT:
value = struct.unpack("<i", bytearray(data))[0]
elif notification_item.plc_datatype == pyads.PLCTYPE_UDINT:
value = struct.unpack("<I", bytearray(data))[0]
else:
value = bytearray(data)
_LOGGER.warning("No callback available for this datatype")
notification_item.callback(notification_item.name, value)
class AdsEntity(Entity):
"""Representation of ADS entity."""
_attr_should_poll = False
def __init__(self, ads_hub, name, ads_var):
"""Initialize ADS binary sensor."""
self._state_dict = {}
self._state_dict[STATE_KEY_STATE] = None
self._ads_hub = ads_hub
self._ads_var = ads_var
self._event = None
self._attr_unique_id = ads_var
self._attr_name = name
async def async_initialize_device(
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None
):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug("Variable %s changed its value to %d", name, value)
if factor is None:
self._state_dict[state_key] = value
else:
self._state_dict[state_key] = value / factor
asyncio.run_coroutine_threadsafe(async_event_set(), self.hass.loop)
self.schedule_update_ha_state()
async def async_event_set():
"""Set event in async context."""
self._event.set()
self._event = asyncio.Event()
await self.hass.async_add_executor_job(
self._ads_hub.add_device_notification, ads_var, plctype, update
)
try:
async with timeout(10):
await self._event.wait()
except TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
@property
def available(self) -> bool:
"""Return False if state has not been updated yet."""
return self._state_dict[STATE_KEY_STATE] is not None

View File

@@ -17,7 +17,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
from .entity import AdsEntity
from .hub import AdsHub
DEFAULT_NAME = "ADS binary sensor" DEFAULT_NAME = "ADS binary sensor"
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
@@ -36,11 +38,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Binary Sensor platform for ADS.""" """Set up the Binary Sensor platform for ADS."""
ads_hub = hass.data.get(DATA_ADS) ads_hub = hass.data[DATA_ADS]
ads_var = config[CONF_ADS_VAR] ads_var: str = config[CONF_ADS_VAR]
name = config[CONF_NAME] name: str = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS) device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class) ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
add_entities([ads_sensor]) add_entities([ads_sensor])
@@ -49,7 +51,13 @@ def setup_platform(
class AdsBinarySensor(AdsEntity, BinarySensorEntity): class AdsBinarySensor(AdsEntity, BinarySensorEntity):
"""Representation of ADS binary sensors.""" """Representation of ADS binary sensors."""
def __init__(self, ads_hub, name, ads_var, device_class): def __init__(
self,
ads_hub: AdsHub,
name: str,
ads_var: str,
device_class: BinarySensorDeviceClass | None,
) -> None:
"""Initialize ADS binary sensor.""" """Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var) super().__init__(ads_hub, name, ads_var)
self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING

View File

@@ -0,0 +1,41 @@
"""Support for Automation Device Specification (ADS)."""
from __future__ import annotations
from enum import StrEnum
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from .hub import AdsHub
DOMAIN = "ads"
DATA_ADS: HassKey[AdsHub] = HassKey(DOMAIN)
CONF_ADS_VAR = "adsvar"
STATE_KEY_STATE = "state"
class AdsType(StrEnum):
"""Supported Types."""
BOOL = "bool"
BYTE = "byte"
INT = "int"
UINT = "uint"
SINT = "sint"
USINT = "usint"
DINT = "dint"
UDINT = "udint"
WORD = "word"
DWORD = "dword"
LREAL = "lreal"
REAL = "real"
STRING = "string"
TIME = "time"
DATE = "date"
DATE_AND_TIME = "dt"
TOD = "tod"

View File

@@ -11,6 +11,7 @@ from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
CoverDeviceClass,
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
) )
@@ -20,14 +21,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import ( from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
CONF_ADS_VAR, from .entity import AdsEntity
CONF_ADS_VAR_POSITION, from .hub import AdsHub
DATA_ADS,
STATE_KEY_POSITION,
STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS Cover" DEFAULT_NAME = "ADS Cover"
@@ -35,6 +31,9 @@ CONF_ADS_VAR_SET_POS = "adsvar_set_position"
CONF_ADS_VAR_OPEN = "adsvar_open" CONF_ADS_VAR_OPEN = "adsvar_open"
CONF_ADS_VAR_CLOSE = "adsvar_close" CONF_ADS_VAR_CLOSE = "adsvar_close"
CONF_ADS_VAR_STOP = "adsvar_stop" CONF_ADS_VAR_STOP = "adsvar_stop"
CONF_ADS_VAR_POSITION = "adsvar_position"
STATE_KEY_POSITION = "position"
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
{ {
@@ -59,14 +58,14 @@ def setup_platform(
"""Set up the cover platform for ADS.""" """Set up the cover platform for ADS."""
ads_hub = hass.data[DATA_ADS] ads_hub = hass.data[DATA_ADS]
ads_var_is_closed = config.get(CONF_ADS_VAR) ads_var_is_closed: str = config[CONF_ADS_VAR]
ads_var_position = config.get(CONF_ADS_VAR_POSITION) ads_var_position: str | None = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS) ads_var_pos_set: str | None = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open = config.get(CONF_ADS_VAR_OPEN) ads_var_open: str | None = config.get(CONF_ADS_VAR_OPEN)
ads_var_close = config.get(CONF_ADS_VAR_CLOSE) ads_var_close: str | None = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop = config.get(CONF_ADS_VAR_STOP) ads_var_stop: str | None = config.get(CONF_ADS_VAR_STOP)
name = config[CONF_NAME] name: str = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS) device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS)
add_entities( add_entities(
[ [
@@ -90,16 +89,16 @@ class AdsCover(AdsEntity, CoverEntity):
def __init__( def __init__(
self, self,
ads_hub, ads_hub: AdsHub,
ads_var_is_closed, ads_var_is_closed: str,
ads_var_position, ads_var_position: str | None,
ads_var_pos_set, ads_var_pos_set: str | None,
ads_var_open, ads_var_open: str | None,
ads_var_close, ads_var_close: str | None,
ads_var_stop, ads_var_stop: str | None,
name, name: str,
device_class, device_class: CoverDeviceClass | None,
): ) -> None:
"""Initialize AdsCover entity.""" """Initialize AdsCover entity."""
super().__init__(ads_hub, name, ads_var_is_closed) super().__init__(ads_hub, name, ads_var_is_closed)
if self._attr_unique_id is None: if self._attr_unique_id is None:

View File

@@ -0,0 +1,70 @@
"""Support for Automation Device Specification (ADS)."""
import asyncio
from asyncio import timeout
import logging
from typing import Any
from homeassistant.helpers.entity import Entity
from .const import STATE_KEY_STATE
from .hub import AdsHub
_LOGGER = logging.getLogger(__name__)
class AdsEntity(Entity):
"""Representation of ADS entity."""
_attr_should_poll = False
def __init__(self, ads_hub: AdsHub, name: str, ads_var: str) -> None:
"""Initialize ADS binary sensor."""
self._state_dict: dict[str, Any] = {}
self._state_dict[STATE_KEY_STATE] = None
self._ads_hub = ads_hub
self._ads_var = ads_var
self._event: asyncio.Event | None = None
self._attr_unique_id = ads_var
self._attr_name = name
async def async_initialize_device(
self,
ads_var: str,
plctype: type,
state_key: str = STATE_KEY_STATE,
factor: int | None = None,
) -> None:
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug("Variable %s changed its value to %d", name, value)
if factor is None:
self._state_dict[state_key] = value
else:
self._state_dict[state_key] = value / factor
asyncio.run_coroutine_threadsafe(async_event_set(), self.hass.loop)
self.schedule_update_ha_state()
async def async_event_set():
"""Set event in async context."""
self._event.set()
self._event = asyncio.Event()
await self.hass.async_add_executor_job(
self._ads_hub.add_device_notification, ads_var, plctype, update
)
try:
async with timeout(10):
await self._event.wait()
except TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
@property
def available(self) -> bool:
"""Return False if state has not been updated yet."""
return self._state_dict[STATE_KEY_STATE] is not None

View File

@@ -0,0 +1,151 @@
"""Support for Automation Device Specification (ADS)."""
from collections import namedtuple
import ctypes
import logging
import struct
import threading
import pyads
_LOGGER = logging.getLogger(__name__)
# Tuple to hold data needed for notification
NotificationItem = namedtuple( # noqa: PYI024
"NotificationItem", "hnotify huser name plc_datatype callback"
)
class AdsHub:
"""Representation of an ADS connection."""
def __init__(self, ads_client):
"""Initialize the ADS hub."""
self._client = ads_client
self._client.open()
# All ADS devices are registered here
self._devices = []
self._notification_items = {}
self._lock = threading.Lock()
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
_LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values():
_LOGGER.debug(
"Deleting device notification %d, %d",
notification_item.hnotify,
notification_item.huser,
)
try:
self._client.del_device_notification(
notification_item.hnotify, notification_item.huser
)
except pyads.ADSError as err:
_LOGGER.error(err)
try:
self._client.close()
except pyads.ADSError as err:
_LOGGER.error(err)
def register_device(self, device):
"""Register a new device."""
self._devices.append(device)
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
with self._lock:
try:
return self._client.write_by_name(name, value, plc_datatype)
except pyads.ADSError as err:
_LOGGER.error("Error writing %s: %s", name, err)
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
with self._lock:
try:
return self._client.read_by_name(name, plc_datatype)
except pyads.ADSError as err:
_LOGGER.error("Error reading %s: %s", name, err)
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
try:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback
)
except pyads.ADSError as err:
_LOGGER.error("Error subscribing to %s: %s", name, err)
else:
hnotify = int(hnotify)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback
)
_LOGGER.debug(
"Added device notification %d for variable %s", hnotify, name
)
def _device_notification_callback(self, notification, name):
"""Handle device notifications."""
contents = notification.contents
hnotify = int(contents.hNotification)
_LOGGER.debug("Received notification %d", hnotify)
# Get dynamically sized data array
data_size = contents.cbSampleSize
data_address = (
ctypes.addressof(contents)
+ pyads.structs.SAdsNotificationHeader.data.offset
)
data = (ctypes.c_ubyte * data_size).from_address(data_address)
# Acquire notification item
with self._lock:
notification_item = self._notification_items.get(hnotify)
if not notification_item:
_LOGGER.error("Unknown device notification handle: %d", hnotify)
return
# Data parsing based on PLC data type
plc_datatype = notification_item.plc_datatype
unpack_formats = {
pyads.PLCTYPE_BYTE: "<b",
pyads.PLCTYPE_INT: "<h",
pyads.PLCTYPE_UINT: "<H",
pyads.PLCTYPE_SINT: "<b",
pyads.PLCTYPE_USINT: "<B",
pyads.PLCTYPE_DINT: "<i",
pyads.PLCTYPE_UDINT: "<I",
pyads.PLCTYPE_WORD: "<H",
pyads.PLCTYPE_DWORD: "<I",
pyads.PLCTYPE_LREAL: "<d",
pyads.PLCTYPE_REAL: "<f",
pyads.PLCTYPE_TOD: "<i", # Treat as DINT
pyads.PLCTYPE_DATE: "<i", # Treat as DINT
pyads.PLCTYPE_DT: "<i", # Treat as DINT
pyads.PLCTYPE_TIME: "<i", # Treat as DINT
}
if plc_datatype == pyads.PLCTYPE_BOOL:
value = bool(struct.unpack("<?", bytearray(data))[0])
elif plc_datatype == pyads.PLCTYPE_STRING:
value = (
bytearray(data).split(b"\x00", 1)[0].decode("utf-8", errors="ignore")
)
elif plc_datatype in unpack_formats:
value = struct.unpack(unpack_formats[plc_datatype], bytearray(data))[0]
else:
value = bytearray(data)
_LOGGER.warning("No callback available for this datatype")
notification_item.callback(notification_item.name, value)

View File

@@ -1,5 +1,7 @@
{ {
"services": { "services": {
"write_data_by_name": "mdi:pencil" "write_data_by_name": {
"service": "mdi:pencil"
}
} }
} }

View File

@@ -19,14 +19,12 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import ( from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
CONF_ADS_VAR, from .entity import AdsEntity
CONF_ADS_VAR_BRIGHTNESS, from .hub import AdsHub
DATA_ADS,
STATE_KEY_BRIGHTNESS, CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
STATE_KEY_STATE, STATE_KEY_BRIGHTNESS = "brightness"
AdsEntity,
)
DEFAULT_NAME = "ADS Light" DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
@@ -45,11 +43,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the light platform for ADS.""" """Set up the light platform for ADS."""
ads_hub = hass.data.get(DATA_ADS) ads_hub = hass.data[DATA_ADS]
ads_var_enable = config[CONF_ADS_VAR] ads_var_enable: str = config[CONF_ADS_VAR]
ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS) ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS)
name = config[CONF_NAME] name: str = config[CONF_NAME]
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)]) add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
@@ -57,7 +55,13 @@ def setup_platform(
class AdsLight(AdsEntity, LightEntity): class AdsLight(AdsEntity, LightEntity):
"""Representation of ADS light.""" """Representation of ADS light."""
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name): def __init__(
self,
ads_hub: AdsHub,
ads_var_enable: str,
ads_var_brightness: str | None,
name: str,
) -> None:
"""Initialize AdsLight entity.""" """Initialize AdsLight entity."""
super().__init__(ads_hub, name, ads_var_enable) super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None self._state_dict[STATE_KEY_BRIGHTNESS] = None

View File

@@ -1,7 +1,7 @@
{ {
"domain": "ads", "domain": "ads",
"name": "ADS", "name": "ADS",
"codeowners": [], "codeowners": ["@mrpasztoradam"],
"documentation": "https://www.home-assistant.io/integrations/ads", "documentation": "https://www.home-assistant.io/integrations/ads",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyads"], "loggers": ["pyads"],

View File

@@ -0,0 +1,86 @@
"""Support for ADS select entities."""
from __future__ import annotations
import pyads
import voluptuous as vol
from homeassistant.components.select import (
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
SelectEntity,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADS_VAR, DATA_ADS
from .entity import AdsEntity
from .hub import AdsHub
DEFAULT_NAME = "ADS select"
CONF_OPTIONS = "options"
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, [cv.string]),
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an ADS select device."""
ads_hub = hass.data[DATA_ADS]
ads_var: str = config[CONF_ADS_VAR]
name: str = config[CONF_NAME]
options: list[str] = config[CONF_OPTIONS]
entity = AdsSelect(ads_hub, ads_var, name, options)
add_entities([entity])
class AdsSelect(AdsEntity, SelectEntity):
"""Representation of an ADS select entity."""
def __init__(
self,
ads_hub: AdsHub,
ads_var: str,
name: str,
options: list[str],
) -> None:
"""Initialize the AdsSelect entity."""
super().__init__(ads_hub, name, ads_var)
self._attr_options = options
self._attr_current_option = None
async def async_added_to_hass(self) -> None:
"""Register device notification."""
await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_INT)
self._ads_hub.add_device_notification(
self._ads_var, pyads.PLCTYPE_INT, self._handle_ads_value
)
def select_option(self, option: str) -> None:
"""Change the selected option."""
if option in self._attr_options:
index = self._attr_options.index(option)
self._ads_hub.write_by_name(self._ads_var, index, pyads.PLCTYPE_INT)
self._attr_current_option = option
def _handle_ads_value(self, name: str, value: int) -> None:
"""Handle the value update from ADS."""
if 0 <= value < len(self._attr_options):
self._attr_current_option = self._attr_options[value]
self.schedule_update_ha_state()

View File

@@ -5,41 +5,54 @@ from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
SensorDeviceClass,
SensorEntity, SensorEntity,
SensorStateClass,
) )
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .. import ads from . import ADS_TYPEMAP, CONF_ADS_FACTOR, CONF_ADS_TYPE
from . import ( from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsType
ADS_TYPEMAP, from .entity import AdsEntity
CONF_ADS_FACTOR, from .hub import AdsHub
CONF_ADS_TYPE,
CONF_ADS_VAR,
STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS sensor" DEFAULT_NAME = "ADS sensor"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_ADS_VAR): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_FACTOR): cv.positive_int, vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In( vol.Optional(CONF_ADS_TYPE, default=AdsType.INT): vol.All(
[ vol.Coerce(AdsType),
ads.ADSTYPE_INT, vol.In(
ads.ADSTYPE_UINT, [
ads.ADSTYPE_BYTE, AdsType.BOOL,
ads.ADSTYPE_DINT, AdsType.BYTE,
ads.ADSTYPE_UDINT, AdsType.INT,
] AdsType.UINT,
AdsType.SINT,
AdsType.USINT,
AdsType.DINT,
AdsType.UDINT,
AdsType.WORD,
AdsType.DWORD,
AdsType.LREAL,
AdsType.REAL,
]
),
), ),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=""): cv.string, vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
} }
) )
@@ -51,15 +64,26 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up an ADS sensor device.""" """Set up an ADS sensor device."""
ads_hub = hass.data.get(ads.DATA_ADS) ads_hub = hass.data[DATA_ADS]
ads_var = config[CONF_ADS_VAR] ads_var: str = config[CONF_ADS_VAR]
ads_type = config[CONF_ADS_TYPE] ads_type: AdsType = config[CONF_ADS_TYPE]
name = config[CONF_NAME] name: str = config[CONF_NAME]
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) factor: int | None = config.get(CONF_ADS_FACTOR)
factor = config.get(CONF_ADS_FACTOR) device_class: SensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
state_class: SensorStateClass | None = config.get(CONF_STATE_CLASS)
unit_of_measurement: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
entity = AdsSensor(ads_hub, ads_var, ads_type, name, unit_of_measurement, factor) entity = AdsSensor(
ads_hub,
ads_var,
ads_type,
name,
factor,
device_class,
state_class,
unit_of_measurement,
)
add_entities([entity]) add_entities([entity])
@@ -67,12 +91,24 @@ def setup_platform(
class AdsSensor(AdsEntity, SensorEntity): class AdsSensor(AdsEntity, SensorEntity):
"""Representation of an ADS sensor entity.""" """Representation of an ADS sensor entity."""
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): def __init__(
self,
ads_hub: AdsHub,
ads_var: str,
ads_type: AdsType,
name: str,
factor: int | None,
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
) -> None:
"""Initialize AdsSensor entity.""" """Initialize AdsSensor entity."""
super().__init__(ads_hub, name, ads_var) super().__init__(ads_hub, name, ads_var)
self._attr_native_unit_of_measurement = unit_of_measurement
self._ads_type = ads_type self._ads_type = ads_type
self._factor = factor self._factor = factor
self._attr_device_class = device_class
self._attr_state_class = state_class
self._attr_native_unit_of_measurement = unit_of_measurement
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register device notification.""" """Register device notification."""

View File

@@ -17,7 +17,8 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
from .entity import AdsEntity
DEFAULT_NAME = "ADS Switch" DEFAULT_NAME = "ADS Switch"
@@ -36,10 +37,10 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up switch platform for ADS.""" """Set up switch platform for ADS."""
ads_hub = hass.data.get(DATA_ADS) ads_hub = hass.data[DATA_ADS]
name = config[CONF_NAME] name: str = config[CONF_NAME]
ads_var = config[CONF_ADS_VAR] ads_var: str = config[CONF_ADS_VAR]
add_entities([AdsSwitch(ads_hub, name, ads_var)]) add_entities([AdsSwitch(ads_hub, name, ads_var)])

View File

@@ -0,0 +1,84 @@
"""Support for ADS valves."""
from __future__ import annotations
import pyads
import voluptuous as vol
from homeassistant.components.valve import (
DEVICE_CLASSES_SCHEMA as VALVE_DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as VALVE_PLATFORM_SCHEMA,
ValveDeviceClass,
ValveEntity,
ValveEntityFeature,
)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADS_VAR, DATA_ADS
from .entity import AdsEntity
from .hub import AdsHub
DEFAULT_NAME = "ADS valve"
PLATFORM_SCHEMA = VALVE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): VALVE_DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an ADS valve device."""
ads_hub = hass.data[DATA_ADS]
ads_var: str = config[CONF_ADS_VAR]
name: str = config[CONF_NAME]
device_class: ValveDeviceClass | None = config.get(CONF_DEVICE_CLASS)
entity = AdsValve(ads_hub, ads_var, name, device_class)
add_entities([entity])
class AdsValve(AdsEntity, ValveEntity):
"""Representation of an ADS valve entity."""
_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
def __init__(
self,
ads_hub: AdsHub,
ads_var: str,
name: str,
device_class: ValveDeviceClass | None,
) -> None:
"""Initialize AdsValve entity."""
super().__init__(ads_hub, name, ads_var)
self._attr_device_class = device_class
self._attr_reports_position = False
self._attr_is_closed = True
async def async_added_to_hass(self) -> None:
"""Register device notification."""
await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL)
def open_valve(self, **kwargs) -> None:
"""Open the valve."""
self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL)
self._attr_is_closed = False
def close_valve(self, **kwargs) -> None:
"""Close the valve."""
self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL)
self._attr_is_closed = True

View File

@@ -1,5 +1,7 @@
{ {
"services": { "services": {
"set_time_to": "mdi:timer-cog" "set_time_to": {
"service": "mdi:timer-cog"
}
} }
} }

View File

@@ -6,7 +6,7 @@ from typing import Any
from aemet_opendata.const import AOD_COORDS from aemet_opendata.const import AOD_COORDS
from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,

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.3"] "requirements": ["AEMET-OpenData==0.5.4"]
} }

View File

@@ -7,7 +7,11 @@
} }
}, },
"services": { "services": {
"add_tracking": "mdi:package-variant-plus", "add_tracking": {
"remove_tracking": "mdi:package-variant-minus" "service": "mdi:package-variant-plus"
},
"remove_tracking": {
"service": "mdi:package-variant-minus"
}
} }
} }

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

@@ -1,9 +1,19 @@
{ {
"services": { "services": {
"start_recording": "mdi:record-rec", "start_recording": {
"stop_recording": "mdi:stop", "service": "mdi:record-rec"
"enable_alerts": "mdi:bell-alert", },
"disable_alerts": "mdi:bell-off", "stop_recording": {
"snapshot": "mdi:camera" "service": "mdi:stop"
},
"enable_alerts": {
"service": "mdi:bell-alert"
},
"disable_alerts": {
"service": "mdi:bell-off"
},
"snapshot": {
"service": "mdi:camera"
}
} }
} }

View File

@@ -2,18 +2,14 @@
from __future__ import annotations from __future__ import annotations
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
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .coordinator import AirGradientCoordinator
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.BUTTON, Platform.BUTTON,
@@ -21,18 +17,11 @@ PLATFORMS: list[Platform] = [
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.UPDATE,
] ]
@dataclass type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
class AirGradientData:
"""AirGradient data class."""
measurement: AirGradientMeasurementCoordinator
config: AirGradientConfigCoordinator
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
@@ -42,27 +31,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
entry.data[CONF_HOST], session=async_get_clientsession(hass) entry.data[CONF_HOST], session=async_get_clientsession(hass)
) )
measurement_coordinator = AirGradientMeasurementCoordinator(hass, client) coordinator = AirGradientCoordinator(hass, client)
config_coordinator = AirGradientConfigCoordinator(hass, client)
await measurement_coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
await config_coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass) entry.runtime_data = coordinator
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
manufacturer="AirGradient",
model=get_model_name(measurement_coordinator.data.model),
model_id=measurement_coordinator.data.model,
serial_number=measurement_coordinator.data.serial_number,
sw_version=measurement_coordinator.data.firmware_version,
)
entry.runtime_data = AirGradientData(
measurement=measurement_coordinator,
config=config_coordinator,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN, AirGradientConfigEntry from . import AirGradientConfigEntry
from .coordinator import AirGradientConfigCoordinator from .const import DOMAIN
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -47,8 +48,8 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AirGradient button entities based on a config entry.""" """Set up AirGradient button entities based on a config entry."""
model = entry.runtime_data.measurement.data.model coordinator = entry.runtime_data
coordinator = entry.runtime_data.config model = coordinator.data.measures.model
added_entities = False added_entities = False
@@ -57,7 +58,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)] entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
@@ -67,7 +68,8 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -87,11 +89,10 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
"""Defines an AirGradient button.""" """Defines an AirGradient button."""
entity_description: AirGradientButtonEntityDescription entity_description: AirGradientButtonEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientConfigCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientButtonEntityDescription, description: AirGradientButtonEntityDescription,
) -> None: ) -> None:
"""Initialize airgradient button.""" """Initialize airgradient button."""

View File

@@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
except AirGradientError: except AirGradientError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
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

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -16,7 +17,15 @@ if TYPE_CHECKING:
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]): @dataclass
class AirGradientData:
"""Class for AirGradient data."""
measures: Measures
config: Config
class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
"""Class to manage fetching AirGradient data.""" """Class to manage fetching AirGradient data."""
config_entry: AirGradientConfigEntry config_entry: AirGradientConfigEntry
@@ -33,25 +42,11 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
assert self.config_entry.unique_id assert self.config_entry.unique_id
self.serial_number = self.config_entry.unique_id self.serial_number = self.config_entry.unique_id
async def _async_update_data(self) -> _DataT: async def _async_update_data(self) -> AirGradientData:
try: try:
return await self._update_data() measures = await self.client.get_current_measures()
config = await self.client.get_config()
except AirGradientError as error: except AirGradientError as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
else:
async def _update_data(self) -> _DataT: return AirGradientData(measures, config)
raise NotImplementedError
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
"""Class to manage fetching AirGradient data."""
async def _update_data(self) -> Measures:
return await self.client.get_current_measures()
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
"""Class to manage fetching AirGradient data."""
async def _update_data(self) -> Config:
return await self.client.get_config()

View File

@@ -1,5 +1,7 @@
"""Base class for AirGradient entities.""" """Base class for AirGradient entities."""
from airgradient import get_model_name
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -15,6 +17,12 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
def __init__(self, coordinator: AirGradientCoordinator) -> None: def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize airgradient entity.""" """Initialize airgradient entity."""
super().__init__(coordinator) super().__init__(coordinator)
measures = coordinator.data.measures
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.serial_number)}, identifiers={(DOMAIN, coordinator.serial_number)},
manufacturer="AirGradient",
model=get_model_name(measures.model),
model_id=measures.model,
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
) )

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.7.1"], "requirements": ["airgradient==0.8.0"],
"zeroconf": ["_airgradient._tcp.local."] "zeroconf": ["_airgradient._tcp.local."]
} }

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -62,8 +62,8 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AirGradient number entities based on a config entry.""" """Set up AirGradient number entities based on a config entry."""
model = entry.runtime_data.measurement.data.model coordinator = entry.runtime_data
coordinator = entry.runtime_data.config model = coordinator.data.measures.model
added_entities = False added_entities = False
@@ -72,7 +72,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities = [] entities = []
@@ -84,7 +84,8 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -104,11 +105,10 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
"""Defines an AirGradient number entity.""" """Defines an AirGradient number entity."""
entity_description: AirGradientNumberEntityDescription entity_description: AirGradientNumberEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientConfigCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientNumberEntityDescription, description: AirGradientNumberEntityDescription,
) -> None: ) -> None:
"""Initialize AirGradient number.""" """Initialize AirGradient number."""
@@ -119,7 +119,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
@property @property
def native_value(self) -> int | None: def native_value(self) -> int | None:
"""Return the state of the number.""" """Return the state of the number."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.config)
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Set the selected value.""" """Set the selected value."""

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
from .coordinator import AirGradientConfigCoordinator from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -144,13 +144,11 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AirGradient select entities based on a config entry.""" """Set up AirGradient select entities based on a config entry."""
coordinator = entry.runtime_data.config coordinator = entry.runtime_data
measurement_coordinator = entry.runtime_data.measurement model = coordinator.data.measures.model
async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)]) async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)])
model = measurement_coordinator.data.model
added_entities = False added_entities = False
@callback @callback
@@ -158,7 +156,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities: list[AirGradientSelect] = [ entities: list[AirGradientSelect] = [
@@ -179,7 +177,8 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -201,11 +200,10 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
"""Defines an AirGradient select entity.""" """Defines an AirGradient select entity."""
entity_description: AirGradientSelectEntityDescription entity_description: AirGradientSelectEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientConfigCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientSelectEntityDescription, description: AirGradientSelectEntityDescription,
) -> None: ) -> None:
"""Initialize AirGradient select.""" """Initialize AirGradient select."""
@@ -216,7 +214,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
"""Return the state of the select.""" """Return the state of the select."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.config)
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""

View File

@@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
from .const import PM_STANDARD, PM_STANDARD_REVERSE from .const import PM_STANDARD, PM_STANDARD_REVERSE
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -218,7 +218,7 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up AirGradient sensor entities based on a config entry.""" """Set up AirGradient sensor entities based on a config entry."""
coordinator = entry.runtime_data.measurement coordinator = entry.runtime_data
listener: Callable[[], None] | None = None listener: Callable[[], None] | None = None
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set( not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
MEASUREMENT_SENSOR_TYPES MEASUREMENT_SENSOR_TYPES
@@ -232,7 +232,7 @@ async def async_setup_entry(
not_setup = set() not_setup = set()
sensors = [] sensors = []
for description in sensor_descriptions: for description in sensor_descriptions:
if description.value_fn(coordinator.data) is None: if description.value_fn(coordinator.data.measures) is None:
not_setup.add(description) not_setup.add(description)
else: else:
sensors.append(AirGradientMeasurementSensor(coordinator, description)) sensors.append(AirGradientMeasurementSensor(coordinator, description))
@@ -248,64 +248,65 @@ async def async_setup_entry(
add_entities() add_entities()
entities = [ entities = [
AirGradientConfigSensor(entry.runtime_data.config, description) AirGradientConfigSensor(coordinator, description)
for description in CONFIG_SENSOR_TYPES for description in CONFIG_SENSOR_TYPES
] ]
if "L" in coordinator.data.model: if "L" in coordinator.data.measures.model:
entities.extend( entities.extend(
AirGradientConfigSensor(entry.runtime_data.config, description) AirGradientConfigSensor(coordinator, description)
for description in CONFIG_LED_BAR_SENSOR_TYPES for description in CONFIG_LED_BAR_SENSOR_TYPES
) )
if "I" in coordinator.data.model: if "I" in coordinator.data.measures.model:
entities.extend( entities.extend(
AirGradientConfigSensor(entry.runtime_data.config, description) AirGradientConfigSensor(coordinator, description)
for description in CONFIG_DISPLAY_SENSOR_TYPES for description in CONFIG_DISPLAY_SENSOR_TYPES
) )
async_add_entities(entities) async_add_entities(entities)
class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity): class AirGradientSensor(AirGradientEntity, SensorEntity):
"""Defines an AirGradient sensor.""" """Defines an AirGradient sensor."""
entity_description: AirGradientMeasurementSensorEntityDescription
coordinator: AirGradientMeasurementCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientMeasurementCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientMeasurementSensorEntityDescription, description: SensorEntityDescription,
) -> None: ) -> None:
"""Initialize airgradient sensor.""" """Initialize airgradient sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
class AirGradientMeasurementSensor(AirGradientSensor):
"""Defines an AirGradient sensor."""
entity_description: AirGradientMeasurementSensorEntityDescription
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.measures)
class AirGradientConfigSensor(AirGradientEntity, SensorEntity): class AirGradientConfigSensor(AirGradientSensor):
"""Defines an AirGradient sensor.""" """Defines an AirGradient sensor."""
entity_description: AirGradientConfigSensorEntityDescription entity_description: AirGradientConfigSensorEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientConfigCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientConfigSensorEntityDescription, description: AirGradientConfigSensorEntityDescription,
) -> None: ) -> None:
"""Initialize airgradient sensor.""" """Initialize airgradient sensor."""
super().__init__(coordinator) super().__init__(coordinator, description)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_entity_registry_enabled_default = ( self._attr_entity_registry_enabled_default = (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
) )
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.config)

View File

@@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -46,7 +46,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AirGradient switch entities based on a config entry.""" """Set up AirGradient switch entities based on a config entry."""
coordinator = entry.runtime_data.config coordinator = entry.runtime_data
added_entities = False added_entities = False
@@ -55,7 +55,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.configuration_control is ConfigurationControl.LOCAL coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
async_add_entities( async_add_entities(
@@ -63,7 +63,8 @@ async def async_setup_entry(
) )
added_entities = True added_entities = True
elif ( elif (
coordinator.data.configuration_control is not ConfigurationControl.LOCAL coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -82,11 +83,10 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
"""Defines an AirGradient switch entity.""" """Defines an AirGradient switch entity."""
entity_description: AirGradientSwitchEntityDescription entity_description: AirGradientSwitchEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientConfigCoordinator, coordinator: AirGradientCoordinator,
description: AirGradientSwitchEntityDescription, description: AirGradientSwitchEntityDescription,
) -> None: ) -> None:
"""Initialize AirGradient switch.""" """Initialize AirGradient switch."""
@@ -97,7 +97,7 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the switch.""" """Return the state of the switch."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data.config)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""

View File

@@ -0,0 +1,54 @@
"""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, AirGradientCoordinator
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."""
coordinator = config_entry.runtime_data
async_add_entities([AirGradientUpdate(coordinator)], True)
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
def __init__(self, coordinator: AirGradientCoordinator) -> 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.measures.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

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==0.9.0"] "requirements": ["airthings-ble==0.9.1"]
} }

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

@@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
_LOGGER.debug("Argument `temperature` is missing in set_temperature") _LOGGER.debug("Argument `temperature` is missing in set_temperature")
return return
await self._control(temp=temp) await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
class Airtouch5Zone(Airtouch5ClimateEntity): class Airtouch5Zone(Airtouch5ClimateEntity):

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,
) )
@@ -54,6 +53,8 @@ from .const import (
LOGGER, LOGGER,
) )
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
# We use a raw string for the airvisual_pro domain (instead of importing the actual # We use a raw string for the airvisual_pro domain (instead of importing the actual
# constant) so that we can avoid listing it as a dependency: # constant) so that we can avoid listing it as a dependency:
DOMAIN_AIRVISUAL_PRO = "airvisual_pro" DOMAIN_AIRVISUAL_PRO = "airvisual_pro"
@@ -62,8 +63,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(
@@ -94,10 +93,9 @@ def async_get_cloud_coordinators_by_api_key(
) -> list[DataUpdateCoordinator]: ) -> list[DataUpdateCoordinator]:
"""Get all DataUpdateCoordinator objects related to a particular API key.""" """Get all DataUpdateCoordinator objects related to a particular API key."""
return [ return [
coordinator entry.runtime_data
for entry_id, coordinator in hass.data[DOMAIN].items() for entry in hass.config_entries.async_entries(DOMAIN)
if (entry := hass.config_entries.async_get_entry(entry_id)) if entry.data.get(CONF_API_KEY) == api_key and hasattr(entry, "runtime_data")
and entry.data.get(CONF_API_KEY) == api_key
] ]
@@ -175,7 +173,7 @@ def _standardize_geography_config_entry(
hass.config_entries.async_update_entry(entry, **entry_updates) hass.config_entries.async_update_entry(entry, **entry_updates)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
"""Set up AirVisual as config entry.""" """Set up AirVisual as config entry."""
if CONF_API_KEY not in entry.data: if CONF_API_KEY not in entry.data:
# If this is a migrated AirVisual Pro entry, there's no actual setup to do; # If this is a migrated AirVisual Pro entry, there's no actual setup to do;
@@ -223,8 +221,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(entry.add_update_listener(async_reload_entry)) entry.async_on_unload(entry.add_update_listener(async_reload_entry))
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
# Reassess the interval between 2 server requests # Reassess the interval between 2 server requests
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
@@ -234,7 +231,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
"""Migrate an old config entry.""" """Migrate an old config entry."""
version = entry.version version = entry.version
@@ -391,21 +388,19 @@ async def async_migrate_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: AirVisualConfigEntry) -> bool:
"""Unload an AirVisual config entry.""" """Unload an AirVisual config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok and CONF_API_KEY in entry.data:
hass.data[DOMAIN].pop(entry.entry_id) # Re-calculate the update interval period for any remaining consumers of
if CONF_API_KEY in entry.data: # this API key:
# Re-calculate the update interval period for any remaining consumers of async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
# this API key:
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
return unload_ok return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_reload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> None:
"""Handle an options update.""" """Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_COUNTRY, CONF_COUNTRY,
@@ -15,9 +14,9 @@ from homeassistant.const import (
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_CITY, DOMAIN from . import AirVisualConfigEntry
from .const import CONF_CITY
CONF_COORDINATES = "coordinates" CONF_COORDINATES = "coordinates"
CONF_TITLE = "title" CONF_TITLE = "title"
@@ -37,10 +36,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: AirVisualConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
return { return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT), "entry": async_redact_data(entry.as_dict(), TO_REDACT),

View File

@@ -26,8 +26,8 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import AirVisualEntity from . import AirVisualConfigEntry, AirVisualEntity
from .const import CONF_CITY, DOMAIN from .const import CONF_CITY
ATTR_CITY = "city" ATTR_CITY = "city"
ATTR_COUNTRY = "country" ATTR_COUNTRY = "country"
@@ -105,10 +105,12 @@ POLLUTANT_UNITS = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: AirVisualConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AirVisual sensors based on a config entry.""" """Set up AirVisual sensors based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
AirVisualGeographySensor(coordinator, entry, description, locale) AirVisualGeographySensor(coordinator, entry, description, locale)
for locale in GEOGRAPHY_SENSOR_LOCALES for locale in GEOGRAPHY_SENSOR_LOCALES

View File

@@ -80,11 +80,9 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
"""Initialize.""" """Initialize."""
self._reauth_entry: ConfigEntry | None = None self._reauth_entry: ConfigEntry | None = None
async def async_step_import( async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
self, import_config: dict[str, Any] """Import a config entry from `airvisual` integration (see #83882)."""
) -> ConfigFlowResult: return await self.async_step_user(import_data)
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
async def async_step_reauth( async def async_step_reauth(
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]

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

@@ -6,7 +6,7 @@ from typing import Any
from aioairzone.const import API_MAC, AZD_MAC from aioairzone.const import API_MAC, AZD_MAC
from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_UNIQUE_ID from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

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.8.1"] "requirements": ["aioairzone==0.9.3"]
} }

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,
@@ -95,7 +132,20 @@ async def async_setup_entry(
received_zones = set(zones_data) received_zones = set(zones_data)
new_zones = received_zones - added_zones new_zones = received_zones - added_zones
if new_zones: if new_zones:
async_add_entities( entities: list[AirzoneZoneSelect] = [
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( AirzoneZoneSelect(
coordinator, coordinator,
description, description,
@@ -106,7 +156,8 @@ async def async_setup_entry(
for system_zone_id in new_zones for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id) if description.key in zones_data.get(system_zone_id)
) ]
async_add_entities(entities)
added_zones.update(new_zones) added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener)) entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
@@ -153,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

@@ -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

@@ -161,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

@@ -21,7 +21,7 @@ from aioairzone_cloud.const import (
RAW_WEBSERVERS, RAW_WEBSERVERS,
) )
from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

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

View File

@@ -12,7 +12,16 @@ from aioairzone_cloud.const import (
AZD_AQ_PM_10, AZD_AQ_PM_10,
AZD_CPU_USAGE, AZD_CPU_USAGE,
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_INDOOR_EXCHANGER_TEMP,
AZD_INDOOR_RETURN_TEMP,
AZD_INDOOR_WORK_TEMP,
AZD_MEMORY_FREE, AZD_MEMORY_FREE,
AZD_OUTDOOR_CONDENSER_PRESS,
AZD_OUTDOOR_DISCHARGE_TEMP,
AZD_OUTDOOR_ELECTRIC_CURRENT,
AZD_OUTDOOR_EVAPORATOR_PRESS,
AZD_OUTDOOR_EXCHANGER_TEMP,
AZD_OUTDOOR_TEMP,
AZD_TEMP, AZD_TEMP,
AZD_THERMOSTAT_BATTERY, AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_COVERAGE, AZD_THERMOSTAT_COVERAGE,
@@ -32,7 +41,9 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory, EntityCategory,
UnitOfElectricCurrent,
UnitOfInformation, UnitOfInformation,
UnitOfPressure,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@@ -48,6 +59,78 @@ from .entity import (
) )
AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_INDOOR_EXCHANGER_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="indoor_exchanger_temp",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_INDOOR_RETURN_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="indoor_return_temp",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_INDOOR_WORK_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="indoor_work_temp",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_CONDENSER_PRESS,
native_unit_of_measurement=UnitOfPressure.KPA,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_condenser_press",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_DISCHARGE_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_discharge_temp",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_ELECTRIC_CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_electric_current",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_EVAPORATOR_PRESS,
native_unit_of_measurement=UnitOfPressure.KPA,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_evaporator_press",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_EXCHANGER_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_exchanger_temp",
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_OUTDOOR_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
translation_key="outdoor_temp",
),
SensorEntityDescription( SensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
key=AZD_TEMP, key=AZD_TEMP,
@@ -189,6 +272,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

@@ -45,6 +45,33 @@
"free_memory": { "free_memory": {
"name": "Free memory" "name": "Free memory"
}, },
"indoor_exchanger_temp": {
"name": "Indoor exchanger temperature"
},
"indoor_return_temp": {
"name": "Indoor return temperature"
},
"indoor_work_temp": {
"name": "Indoor working temperature"
},
"outdoor_condenser_press": {
"name": "Outdoor condenser pressure"
},
"outdoor_discharge_temp": {
"name": "Outdoor discharge temperature"
},
"outdoor_electric_current": {
"name": "Outdoor electric current"
},
"outdoor_evaporator_press": {
"name": "Outdoor evaporator pressure"
},
"outdoor_exchanger_temp": {
"name": "Outdoor exchanger temperature"
},
"outdoor_temp": {
"name": "Outdoor temperature"
},
"thermostat_coverage": { "thermostat_coverage": {
"name": "Signal percentage" "name": "Signal percentage"
} }

View File

@@ -15,12 +15,26 @@
} }
}, },
"services": { "services": {
"alarm_arm_away": "mdi:shield-lock", "alarm_arm_away": {
"alarm_arm_home": "mdi:shield-home", "service": "mdi:shield-lock"
"alarm_arm_night": "mdi:shield-moon", },
"alarm_arm_custom_bypass": "mdi:security", "alarm_arm_home": {
"alarm_disarm": "mdi:shield-off", "service": "mdi:shield-home"
"alarm_trigger": "mdi:bell-ring", },
"alarm_arm_vacation": "mdi:shield-airplane" "alarm_arm_night": {
"service": "mdi:shield-moon"
},
"alarm_arm_custom_bypass": {
"service": "mdi:security"
},
"alarm_disarm": {
"service": "mdi:shield-off"
},
"alarm_trigger": {
"service": "mdi:bell-ring"
},
"alarm_arm_vacation": {
"service": "mdi:shield-airplane"
}
} }
} }

View File

@@ -7,7 +7,11 @@
} }
}, },
"services": { "services": {
"alarm_keypress": "mdi:dialpad", "alarm_keypress": {
"alarm_toggle_chime": "mdi:abc" "service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
}
} }
} }

View File

@@ -2,18 +2,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TITLE,
DOMAIN as DOMAIN_NOTIFY,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_NAME, CONF_NAME,
@@ -22,22 +12,12 @@ from homeassistant.const import (
SERVICE_TOGGLE, SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_IDLE,
STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_state_change_event,
)
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.dt import now
from .const import ( from .const import (
CONF_ALERT_MESSAGE, CONF_ALERT_MESSAGE,
@@ -52,6 +32,7 @@ from .const import (
DOMAIN, DOMAIN,
LOGGER, LOGGER,
) )
from .entity import AlertEntity
ALERT_SCHEMA = vol.Schema( ALERT_SCHEMA = vol.Schema(
{ {
@@ -83,9 +64,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Alert component.""" """Set up the Alert component."""
component = EntityComponent[Alert](LOGGER, DOMAIN, hass) component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
entities: list[Alert] = [] entities: list[AlertEntity] = []
for object_id, cfg in config[DOMAIN].items(): for object_id, cfg in config[DOMAIN].items():
if not cfg: if not cfg:
@@ -104,7 +85,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
data = cfg.get(CONF_DATA) data = cfg.get(CONF_DATA)
entities.append( entities.append(
Alert( AlertEntity(
hass, hass,
object_id, object_id,
name, name,
@@ -124,198 +105,10 @@ 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)
return True return True
class Alert(Entity):
"""Representation of an alert."""
_attr_should_poll = False
def __init__(
self,
hass: HomeAssistant,
entity_id: str,
name: str,
watched_entity_id: str,
state: str,
repeat: list[float],
skip_first: bool,
message_template: Template | None,
done_message_template: Template | None,
notifiers: list[str],
can_ack: bool,
title_template: Template | None,
data: dict[Any, Any],
) -> None:
"""Initialize the alert."""
self.hass = hass
self._attr_name = name
self._alert_state = state
self._skip_first = skip_first
self._data = data
self._message_template = message_template
if self._message_template is not None:
self._message_template.hass = hass
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
if self._title_template is not None:
self._title_template.hass = hass
self._notifiers = notifiers
self._can_ack = can_ack
self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0
self._firing = False
self._ack = False
self._cancel: Callable[[], None] | None = None
self._send_done_message = False
self.entity_id = f"{DOMAIN}.{entity_id}"
async_track_state_change_event(
hass, [watched_entity_id], self.watched_entity_change
)
@property
def state(self) -> str:
"""Return the alert status."""
if self._firing:
if self._ack:
return STATE_OFF
return STATE_ON
return STATE_IDLE
async def watched_entity_change(self, event: Event[EventStateChangedData]) -> None:
"""Determine if the alert should start or stop."""
if (to_state := event.data["new_state"]) is None:
return
LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"])
if to_state.state == self._alert_state and not self._firing:
await self.begin_alerting()
if to_state.state != self._alert_state and self._firing:
await self.end_alerting()
async def begin_alerting(self) -> None:
"""Begin the alert procedures."""
LOGGER.debug("Beginning Alert: %s", self._attr_name)
self._ack = False
self._firing = True
self._next_delay = 0
if not self._skip_first:
await self._notify()
else:
await self._schedule_notify()
self.async_write_ha_state()
async def end_alerting(self) -> None:
"""End the alert procedures."""
LOGGER.debug("Ending Alert: %s", self._attr_name)
if self._cancel is not None:
self._cancel()
self._cancel = None
self._ack = False
self._firing = False
if self._send_done_message:
await self._notify_done_message()
self.async_write_ha_state()
async def _schedule_notify(self) -> None:
"""Schedule a notification."""
delay = self._delay[self._next_delay]
next_msg = now() + delay
self._cancel = async_track_point_in_time(
self.hass,
HassJob(
self._notify, name="Schedule notify alert", cancel_on_shutdown=True
),
next_msg,
)
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
async def _notify(self, *args: Any) -> None:
"""Send the alert notification."""
if not self._firing:
return
if not self._ack:
LOGGER.info("Alerting: %s", self._attr_name)
self._send_done_message = True
if self._message_template is not None:
message = self._message_template.async_render(parse_result=False)
else:
message = self._attr_name
await self._send_notification_message(message)
await self._schedule_notify()
async def _notify_done_message(self) -> None:
"""Send notification of complete alert."""
LOGGER.info("Alerting: %s", self._done_message_template)
self._send_done_message = False
if self._done_message_template is None:
return
message = self._done_message_template.async_render(parse_result=False)
await self._send_notification_message(message)
async def _send_notification_message(self, message: Any) -> None:
if not self._notifiers:
return
msg_payload = {ATTR_MESSAGE: message}
if self._title_template is not None:
title = self._title_template.async_render(parse_result=False)
msg_payload[ATTR_TITLE] = title
if self._data:
msg_payload[ATTR_DATA] = self._data
LOGGER.debug(msg_payload)
for target in self._notifiers:
try:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload, context=self._context
)
except ServiceNotFound:
LOGGER.error(
"Failed to call notify.%s, retrying at next notification interval",
target,
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Async Unacknowledge alert."""
LOGGER.debug("Reset Alert: %s", self._attr_name)
self._ack = False
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Async Acknowledge alert."""
LOGGER.debug("Acknowledged Alert: %s", self._attr_name)
self._ack = True
self.async_write_ha_state()
async def async_toggle(self, **kwargs: Any) -> None:
"""Async toggle alert."""
if self._ack:
return await self.async_turn_on()
return await self.async_turn_off()

View File

@@ -0,0 +1,206 @@
"""Support for repeating alerts when conditions are met."""
from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
from typing import Any
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TITLE,
DOMAIN as DOMAIN_NOTIFY,
)
from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_state_change_event,
)
from homeassistant.helpers.template import Template
from homeassistant.util.dt import now
from .const import DOMAIN, LOGGER
class AlertEntity(Entity):
"""Representation of an alert."""
_attr_should_poll = False
def __init__(
self,
hass: HomeAssistant,
entity_id: str,
name: str,
watched_entity_id: str,
state: str,
repeat: list[float],
skip_first: bool,
message_template: Template | None,
done_message_template: Template | None,
notifiers: list[str],
can_ack: bool,
title_template: Template | None,
data: dict[Any, Any],
) -> None:
"""Initialize the alert."""
self.hass = hass
self._attr_name = name
self._alert_state = state
self._skip_first = skip_first
self._data = data
self._message_template = message_template
self._done_message_template = done_message_template
self._title_template = title_template
self._notifiers = notifiers
self._can_ack = can_ack
self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0
self._firing = False
self._ack = False
self._cancel: Callable[[], None] | None = None
self._send_done_message = False
self.entity_id = f"{DOMAIN}.{entity_id}"
async_track_state_change_event(
hass, [watched_entity_id], self.watched_entity_change
)
@property
def state(self) -> str:
"""Return the alert status."""
if self._firing:
if self._ack:
return STATE_OFF
return STATE_ON
return STATE_IDLE
async def watched_entity_change(self, event: Event[EventStateChangedData]) -> None:
"""Determine if the alert should start or stop."""
if (to_state := event.data["new_state"]) is None:
return
LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"])
if to_state.state == self._alert_state and not self._firing:
await self.begin_alerting()
if to_state.state != self._alert_state and self._firing:
await self.end_alerting()
async def begin_alerting(self) -> None:
"""Begin the alert procedures."""
LOGGER.debug("Beginning Alert: %s", self._attr_name)
self._ack = False
self._firing = True
self._next_delay = 0
if not self._skip_first:
await self._notify()
else:
await self._schedule_notify()
self.async_write_ha_state()
async def end_alerting(self) -> None:
"""End the alert procedures."""
LOGGER.debug("Ending Alert: %s", self._attr_name)
if self._cancel is not None:
self._cancel()
self._cancel = None
self._ack = False
self._firing = False
if self._send_done_message:
await self._notify_done_message()
self.async_write_ha_state()
async def _schedule_notify(self) -> None:
"""Schedule a notification."""
delay = self._delay[self._next_delay]
next_msg = now() + delay
self._cancel = async_track_point_in_time(
self.hass,
HassJob(
self._notify, name="Schedule notify alert", cancel_on_shutdown=True
),
next_msg,
)
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
async def _notify(self, *args: Any) -> None:
"""Send the alert notification."""
if not self._firing:
return
if not self._ack:
LOGGER.info("Alerting: %s", self._attr_name)
self._send_done_message = True
if self._message_template is not None:
message = self._message_template.async_render(parse_result=False)
else:
message = self._attr_name
await self._send_notification_message(message)
await self._schedule_notify()
async def _notify_done_message(self) -> None:
"""Send notification of complete alert."""
LOGGER.info("Alerting: %s", self._done_message_template)
self._send_done_message = False
if self._done_message_template is None:
return
message = self._done_message_template.async_render(parse_result=False)
await self._send_notification_message(message)
async def _send_notification_message(self, message: Any) -> None:
if not self._notifiers:
return
msg_payload = {ATTR_MESSAGE: message}
if self._title_template is not None:
title = self._title_template.async_render(parse_result=False)
msg_payload[ATTR_TITLE] = title
if self._data:
msg_payload[ATTR_DATA] = self._data
LOGGER.debug(msg_payload)
for target in self._notifiers:
try:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload, context=self._context
)
except ServiceNotFound:
LOGGER.error(
"Failed to call notify.%s, retrying at next notification interval",
target,
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Async Unacknowledge alert."""
LOGGER.debug("Reset Alert: %s", self._attr_name)
self._ack = False
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Async Acknowledge alert."""
LOGGER.debug("Acknowledged Alert: %s", self._attr_name)
self._ack = True
self.async_write_ha_state()
async def async_toggle(self, **kwargs: Any) -> None:
"""Async toggle alert."""
if self._ack:
return await self.async_turn_on()
return await self.async_turn_off()

View File

@@ -1,7 +1,13 @@
{ {
"services": { "services": {
"toggle": "mdi:bell-ring", "toggle": {
"turn_off": "mdi:bell-off", "service": "mdi:bell-ring"
"turn_on": "mdi:bell-alert" },
"turn_off": {
"service": "mdi:bell-off"
},
"turn_on": {
"service": "mdi:bell-alert"
}
} }
} }

View File

@@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity):
def interfaces(self) -> Generator[AlexaCapability]: def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
yield AlexaPowerController(self.entity) yield AlexaPowerController(self.entity)
yield AlexaModeController( supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" 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 AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity) yield Alexa(self.entity)

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

@@ -1206,7 +1206,7 @@ async def async_api_set_mode(
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
# Remote Activity # Remote Activity
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}": elif instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = mode.split(".")[1] activity = mode.split(".")[1]
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST) activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
if activity != PRESET_MODE_NA and activities and activity in activities: if activity != PRESET_MODE_NA and activities and activity in activities:

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

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections import defaultdict
import logging import logging
from typing import Any, Final from typing import Any, Final
@@ -16,6 +17,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 +44,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__)
@@ -112,6 +115,8 @@ def get_engine(
all_voices: dict[str, dict[str, str]] = {} all_voices: dict[str, dict[str, str]] = {}
all_engines: dict[str, set[str]] = defaultdict(set)
all_voices_req = polly_client.describe_voices() all_voices_req = polly_client.describe_voices()
for voice in all_voices_req.get("Voices", []): for voice in all_voices_req.get("Voices", []):
@@ -122,8 +127,12 @@ def get_engine(
language_code: str | None = voice.get("LanguageCode") language_code: str | None = voice.get("LanguageCode")
if language_code is not None and language_code not in supported_languages: if language_code is not None and language_code not in supported_languages:
supported_languages.append(language_code) supported_languages.append(language_code)
for engine in voice.get("SupportedEngines"):
all_engines[engine].add(voice_id)
return AmazonPollyProvider(polly_client, config, supported_languages, all_voices) return AmazonPollyProvider(
polly_client, config, supported_languages, all_voices, all_engines
)
class AmazonPollyProvider(Provider): class AmazonPollyProvider(Provider):
@@ -135,13 +144,16 @@ class AmazonPollyProvider(Provider):
config: ConfigType, config: ConfigType,
supported_languages: list[str], supported_languages: list[str],
all_voices: dict[str, dict[str, str]], all_voices: dict[str, dict[str, str]],
all_engines: dict[str, set[str]],
) -> None: ) -> None:
"""Initialize Amazon Polly provider for TTS.""" """Initialize Amazon Polly provider for TTS."""
self.client = polly_client self.client = polly_client
self.config = config self.config = config
self.supported_langs = supported_languages self.supported_langs = supported_languages
self.all_voices = all_voices self.all_voices = all_voices
self.all_engines = all_engines
self.default_voice: str = self.config[CONF_VOICE] self.default_voice: str = self.config[CONF_VOICE]
self.default_engine: str = self.config[CONF_ENGINE]
self.name = "Amazon Polly" self.name = "Amazon Polly"
@property @property
@@ -157,12 +169,12 @@ class AmazonPollyProvider(Provider):
@property @property
def default_options(self) -> dict[str, str]: def default_options(self) -> dict[str, str]:
"""Return dict include default options.""" """Return dict include default options."""
return {CONF_VOICE: self.default_voice} return {CONF_VOICE: self.default_voice, CONF_ENGINE: self.default_engine}
@property @property
def supported_options(self) -> list[str]: def supported_options(self) -> list[str]:
"""Return a list of supported options.""" """Return a list of supported options."""
return [CONF_VOICE] return [CONF_VOICE, CONF_ENGINE]
def get_tts_audio( def get_tts_audio(
self, self,
@@ -177,9 +189,14 @@ class AmazonPollyProvider(Provider):
_LOGGER.error("%s does not support the %s language", voice_id, language) _LOGGER.error("%s does not support the %s language", voice_id, language)
return None, None return None, None
engine = options.get(CONF_ENGINE, self.default_engine)
if voice_id not in self.all_engines[engine]:
_LOGGER.error("%s does not support the %s engine", voice_id, engine)
return None, None
_LOGGER.debug("Requesting TTS file for text: %s", message) _LOGGER.debug("Requesting TTS file for text: %s", message)
resp = self.client.synthesize_speech( resp = self.client.synthesize_speech(
Engine=self.config[CONF_ENGINE], Engine=engine,
OutputFormat=self.config[CONF_OUTPUT_FORMAT], OutputFormat=self.config[CONF_OUTPUT_FORMAT],
SampleRate=self.config[CONF_SAMPLE_RATE], SampleRate=self.config[CONF_SAMPLE_RATE],
Text=message, Text=message,

View File

@@ -7,11 +7,13 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS from .const import CONF_SITE_ID, PLATFORMS
from .coordinator import AmberUpdateCoordinator from .coordinator import AmberUpdateCoordinator
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
"""Set up Amber Electric from a config entry.""" """Set up Amber Electric from a config entry."""
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN]) configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
api_instance = amber_api.AmberApi.create(configuration) api_instance = amber_api.AmberApi.create(configuration)
@@ -19,15 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -8,12 +8,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN from . import AmberConfigEntry
from .const import ATTRIBUTION
from .coordinator import AmberUpdateCoordinator from .coordinator import AmberUpdateCoordinator
PRICE_SPIKE_ICONS = { PRICE_SPIKE_ICONS = {
@@ -85,11 +85,11 @@ class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AmberConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
price_spike_description = BinarySensorEntityDescription( price_spike_description = BinarySensorEntityDescription(
key="price_spike", key="price_spike",

View File

@@ -17,13 +17,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN from . import AmberConfigEntry
from .const import ATTRIBUTION
from .coordinator import AmberUpdateCoordinator, normalize_descriptor from .coordinator import AmberUpdateCoordinator, normalize_descriptor
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
@@ -196,11 +196,11 @@ class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity):
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AmberConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
current: dict[str, CurrentInterval] = coordinator.data["current"] current: dict[str, CurrentInterval] = coordinator.data["current"]
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"] forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]

View File

@@ -8,28 +8,30 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator from .coordinator import AmbientNetworkDataUpdateCoordinator
type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator]
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
) -> bool:
"""Set up the Ambient Weather Network from a config entry.""" """Set up the Ambient Weather Network from a config entry."""
api = OpenAPI() api = OpenAPI()
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api) coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioambient"], "loggers": ["aioambient"],
"requirements": ["aioambient==2024.01.0"] "requirements": ["aioambient==2024.08.0"]
} }

View File

@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
@@ -29,7 +28,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import DOMAIN from . import AmbientNetworkConfigEntry
from .coordinator import AmbientNetworkDataUpdateCoordinator from .coordinator import AmbientNetworkDataUpdateCoordinator
from .entity import AmbientNetworkEntity from .entity import AmbientNetworkEntity
@@ -271,12 +270,12 @@ SENSOR_DESCRIPTIONS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AmbientNetworkConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Ambient Network sensor entities.""" """Set up the Ambient Network sensor entities."""
coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
if coordinator.config_entry is not None: if coordinator.config_entry is not None:
async_add_entities( async_add_entities(
AmbientNetworkSensor( AmbientNetworkSensor(

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