Compare commits

..

1 Commits

Author SHA1 Message Date
G Johansson
45aab80b59 Modify query as template in SQL integration 2024-08-23 14:48:50 +00:00
4671 changed files with 48123 additions and 173703 deletions

View File

@@ -14,7 +14,6 @@ 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/**
@@ -62,7 +61,6 @@ 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/**
@@ -111,7 +109,6 @@ components: &components
- homeassistant/components/tag/** - homeassistant/components/tag/**
- homeassistant/components/template/** - homeassistant/components/template/**
- homeassistant/components/timer/** - homeassistant/components/timer/**
- homeassistant/components/trace/**
- homeassistant/components/usb/** - homeassistant/components/usb/**
- homeassistant/components/webhook/** - homeassistant/components/webhook/**
- homeassistant/components/websocket_api/** - homeassistant/components/websocket_api/**
@@ -127,11 +124,9 @@ 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

@@ -27,12 +27,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
with: with:
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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz
@@ -90,7 +90,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
@@ -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.2.0 uses: actions/setup-python@v5.1.1
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.txt)" python3 -m pip install "$(grep '^uv' < requirements_test.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 }}"
@@ -242,7 +242,7 @@ jobs:
- green - green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Set build additional args - name: Set build additional args
run: | run: |
@@ -279,7 +279,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@@ -316,12 +316,11 @@ 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:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.6.0 uses: sigstore/cosign-installer@v3.6.0
@@ -451,10 +450,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -483,56 +482,3 @@ 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- 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@32945a339266b759abcbdc89316275140b0fc960 # v6.8.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@32945a339266b759abcbdc89316275140b0fc960 # v6.8.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

@@ -39,8 +39,8 @@ on:
env: env:
CACHE_VERSION: 10 CACHE_VERSION: 10
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.11" HA_SHORT_VERSION: "2024.9"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: | run: |
@@ -231,10 +231,10 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -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.txt)" pip install "$(grep '^uv' < requirements_test.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
@@ -277,9 +277,9 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -317,9 +317,9 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -357,9 +357,9 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -429,32 +429,17 @@ 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.2.0
- 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 ${{ matrix.file }} - name: Check Dockerfile
uses: docker://hadolint/hadolint:v2.12.0 uses: docker://hadolint/hadolint:v1.18.2
with: with:
args: hadolint ${{ matrix.file }} args: hadolint Dockerfile
- name: Check Dockerfile.dev
uses: docker://hadolint/hadolint:v1.18.2
with:
args: hadolint Dockerfile.dev
base: base:
name: Prepare dependencies name: Prepare dependencies
@@ -466,17 +451,17 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- 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.txt | grep uv | cut -d '=' -f 3) uv_version=$(cat requirements_test.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
@@ -525,7 +510,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.txt)" pip install "$(grep '^uv' < requirements_test.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
@@ -550,10 +535,10 @@ jobs:
sudo apt-get -y install \ sudo apt-get -y install \
libturbojpeg libturbojpeg
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -583,10 +568,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -617,10 +602,10 @@ jobs:
&& needs.info.outputs.requirements == 'true' && needs.info.outputs.requirements == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -638,14 +623,14 @@ 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.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: licenses name: licenses
path: licenses.json path: licenses.json
- name: Process licenses - name: Process licenses
run: | run: |
. venv/bin/activate . venv/bin/activate
python -m script.licenses licenses.json python -m script.licenses
pylint: pylint:
name: Check pylint name: Check pylint
@@ -660,10 +645,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -707,10 +692,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -752,10 +737,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -815,11 +800,7 @@ jobs:
needs: needs:
- info - info
- base - base
strategy: name: Split tests for full run
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
name: Split tests for full run Python ${{ matrix.python-version }}
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
run: | run: |
@@ -831,12 +812,12 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
@@ -852,7 +833,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.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest_buckets name: pytest_buckets
path: pytest_buckets.txt path: pytest_buckets.txt
@@ -895,10 +876,10 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -953,14 +934,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure' if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@@ -1015,10 +996,10 @@ jobs:
libturbojpeg \ libturbojpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1079,7 +1060,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@@ -1087,7 +1068,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@@ -1141,10 +1122,10 @@ jobs:
libturbojpeg \ libturbojpeg \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1206,7 +1187,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@@ -1214,7 +1195,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@@ -1236,7 +1217,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
with: with:
@@ -1287,10 +1268,10 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@@ -1348,14 +1329,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@@ -1374,7 +1355,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
with: with:

View File

@@ -21,14 +21,14 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.26.9 uses: github/codeql-action/init@v3.26.4
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.26.9 uses: github/codeql-action/analyze@v3.26.4
with: with:
category: "/language:python" category: "/language:python"

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -32,11 +32,11 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 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.2.0 uses: actions/setup-python@v5.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -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.txt)" pip install "$(grep '^uv' < requirements_test.txt)"
uv pip install -r requirements.txt uv pip install -r requirements.txt
- name: Get information - name: Get information
@@ -82,15 +82,14 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
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.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
@@ -102,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels - name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.3.6
with: with:
name: requirements_all_wheels name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt path: ./requirements_all_wheels_*.txt
@@ -119,7 +118,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@@ -131,12 +130,6 @@ 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:
@@ -146,7 +139,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;multidict;yarl skip-binary: aiohttp
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"
@@ -163,7 +156,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.7
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@@ -180,18 +173,6 @@ 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.
@@ -205,11 +186,22 @@ jobs:
# Some dependencies still require 'cython<3' # Some dependencies still require 'cython<3'
# and don't yet use isolated build environments. # and don't yet use isolated build environments.
# Build these first. # Build these first.
# grpcio: https://github.com/grpc/grpc/issues/33918
# pydantic: https://github.com/pydantic/pydantic/issues/7689 # pydantic: https://github.com/pydantic/pydantic/issues/7689
touch requirements_old-cython.txt touch 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:
@@ -219,7 +211,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;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt" requirements: "requirements_old-cython.txt"
@@ -234,7 +226,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;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
@@ -248,7 +240,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;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
@@ -262,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac" requirements: "requirements_all.txtac"

View File

@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.8 rev: v0.6.2
hooks: hooks:
- id: ruff - id: ruff
args: args:
@@ -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.+\.txt)$ files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
- id: hassfest-metadata - id: hassfest-metadata
name: hassfest-metadata name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata

View File

@@ -95,7 +95,6 @@ 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.assist_satellite.*
homeassistant.components.asuswrt.* homeassistant.components.asuswrt.*
homeassistant.components.autarco.* homeassistant.components.autarco.*
homeassistant.components.auth.* homeassistant.components.auth.*
@@ -111,7 +110,6 @@ 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,7 +139,6 @@ 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.*
@@ -211,8 +208,6 @@ 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.*
@@ -283,7 +278,6 @@ 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.*
@@ -300,6 +294,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.manual.*
homeassistant.components.map.* homeassistant.components.map.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
@@ -316,7 +311,6 @@ 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.*
@@ -343,7 +337,6 @@ 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.*
@@ -403,7 +396,6 @@ 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.*
@@ -415,13 +407,10 @@ 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.*
@@ -481,7 +470,6 @@ 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,7 +48,6 @@ 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
@@ -144,8 +143,6 @@ 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
@@ -231,16 +228,14 @@ 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 @thecode /homeassistant/components/bthome/ @Ernst79
/tests/components/bthome/ @Ernst79 @thecode /tests/components/bthome/ @Ernst79
/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
@@ -299,8 +294,6 @@ 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
@@ -360,8 +353,6 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221 /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
@@ -556,14 +547,11 @@ 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 @tronikos /homeassistant/components/google_cloud/ @lufton
/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
@@ -641,8 +629,6 @@ 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
@@ -721,8 +707,8 @@ build.json @home-assistant/supervisor
/tests/components/ios/ @robbiet480 /tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard /tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iotty/ @pburgio @shapournemati-iotty /homeassistant/components/iotty/ @pburgio
/tests/components/iotty/ @pburgio @shapournemati-iotty /tests/components/iotty/ @pburgio
/homeassistant/components/iperf3/ @rohankapoorcom /homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes /homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes /tests/components/ipma/ @dgomes
@@ -735,8 +721,6 @@ 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
@@ -813,8 +797,6 @@ 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/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
@@ -861,8 +843,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 @danaues @eclair4151 /homeassistant/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/tests/components/lutron_caseta/ @swails @danaues @eclair4151 /tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001 /homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea /homeassistant/components/madvr/ @iloveicedgreentea
@@ -925,8 +907,6 @@ 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
@@ -1024,8 +1004,6 @@ build.json @home-assistant/supervisor
/tests/components/nut/ @bdraco @ollo69 @pestevez /tests/components/nut/ @bdraco @ollo69 @pestevez
/homeassistant/components/nws/ @MatthewFlamm @kamiyo /homeassistant/components/nws/ @MatthewFlamm @kamiyo
/tests/components/nws/ @MatthewFlamm @kamiyo /tests/components/nws/ @MatthewFlamm @kamiyo
/homeassistant/components/nyt_games/ @joostlek
/tests/components/nyt_games/ @joostlek
/homeassistant/components/nzbget/ @chriscla /homeassistant/components/nzbget/ @chriscla
/tests/components/nzbget/ @chriscla /tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney /homeassistant/components/obihai/ @dshokouhi @ejpenney
@@ -1104,6 +1082,8 @@ build.json @home-assistant/supervisor
/tests/components/pi_hole/ @shenxn /tests/components/pi_hole/ @shenxn
/homeassistant/components/picnic/ @corneyl /homeassistant/components/picnic/ @corneyl
/tests/components/picnic/ @corneyl /tests/components/picnic/ @corneyl
/homeassistant/components/pilight/ @trekky12
/tests/components/pilight/ @trekky12
/homeassistant/components/ping/ @jpbede /homeassistant/components/ping/ @jpbede
/tests/components/ping/ @jpbede /tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan /homeassistant/components/plaato/ @JohNan
@@ -1133,8 +1113,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/proximity/ @mib1185 /homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185 /tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno /homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/prusalink/ @balloob /homeassistant/components/prusalink/ @balloob @Skaronator
/tests/components/prusalink/ @balloob /tests/components/prusalink/ @balloob @Skaronator
/homeassistant/components/ps4/ @ktnrg45 /homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45 /tests/components/ps4/ @ktnrg45
/homeassistant/components/pure_energie/ @klaasnicolaas /homeassistant/components/pure_energie/ @klaasnicolaas
@@ -1295,8 +1275,6 @@ 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
@@ -1434,10 +1412,10 @@ 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 @Gigatrappeur /homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur /tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza /homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode @YogevBokobza /tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/switchmate/ @danielhiversen @qiz-li
/homeassistant/components/syncthing/ @zhulik /homeassistant/components/syncthing/ @zhulik
/tests/components/syncthing/ @zhulik /tests/components/syncthing/ @zhulik
@@ -1515,8 +1493,6 @@ 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
@@ -1541,8 +1517,6 @@ 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
@@ -1647,8 +1621,6 @@ 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
@@ -1666,8 +1638,6 @@ 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
@@ -1688,8 +1658,6 @@ 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.4.15 RUN pip3 install uv==0.2.27
WORKDIR /usr/src WORKDIR /usr/src
@@ -29,9 +29,15 @@ RUN \
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \ if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
uv pip install homeassistant/home_assistant_*.whl; \ uv pip install homeassistant/home_assistant_*.whl; \
fi \ fi \
&& uv pip install \ && if [ "${BUILD_ARCH}" = "i386" ]; then \
--no-build \ linux32 uv pip install \
-r homeassistant/requirements_all.txt --no-build \
-r homeassistant/requirements_all.txt; \
else \
uv pip install \
--no-build \
-r homeassistant/requirements_all.txt; \
fi
## Setup Home Assistant Core ## Setup Home Assistant Core
COPY . homeassistant/ COPY . homeassistant/

View File

@@ -42,8 +42,7 @@ 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

@@ -7,6 +7,8 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__, demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__. `tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|screenshot-states| |screenshot-states|
Featured integrations Featured integrations
@@ -20,14 +22,9 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
If you run into issues while using Home Assistant or during development If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information. of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|ohf-logo|
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg .. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://www.home-assistant.io/join-chat/ :target: https://www.home-assistant.io/join-chat/
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png .. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
:target: https://demo.home-assistant.io :target: https://demo.home-assistant.io
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png .. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
:target: https://home-assistant.io/integrations/ :target: https://home-assistant.io/integrations/
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
:alt: Home Assistant - A project from the Open Home Foundation
:target: https://www.openhomefoundation.org/

View File

@@ -127,11 +127,7 @@ class AuthManagerFlowManager(
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]], flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
result: AuthFlowResult, result: AuthFlowResult,
) -> AuthFlowResult: ) -> AuthFlowResult:
"""Return a user as result of login flow. """Return a user as result of login flow."""
This method is called when a flow step returns FlowResultType.ABORT or
FlowResultType.CREATE_ENTRY.
"""
flow = cast(LoginFlow, flow) flow = cast(LoginFlow, flow)
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:

View File

@@ -1,5 +0,0 @@
{
"domain": "aqara",
"name": "Aqara",
"iot_standards": ["matter", "zigbee"]
}

View File

@@ -9,7 +9,6 @@
"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 +0,0 @@
{
"domain": "roth",
"name": "Roth",
"integrations": ["touchline", "touchline_sl"]
}

View File

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

View File

@@ -6,3 +6,52 @@ 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,10 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import partial from functools import partial
from pathlib import Path
from jaraco.abode.client import Client as Abode from jaraco.abode.client import Client as Abode
import jaraco.abode.config
from jaraco.abode.exceptions import ( from jaraco.abode.exceptions import (
AuthenticationException as AbodeAuthenticationException, AuthenticationException as AbodeAuthenticationException,
Exception as AbodeException, Exception as AbodeException,
@@ -95,9 +93,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
polling = entry.data[CONF_POLLING] polling = entry.data[CONF_POLLING]
# Configure abode library to use config directory for storing data
jaraco.abode.config.paths.override(user_data=Path(hass.config.path("Abode")))
# For previous config entries where unique_id is None # For previous config entries where unique_id is None
if entry.unique_id is None: if entry.unique_id is None:
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(

View File

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

View File

@@ -7,14 +7,8 @@
} }
}, },
"services": { "services": {
"capture_image": { "capture_image": "mdi:camera",
"service": "mdi:camera" "change_setting": "mdi:cog",
}, "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==6.2.1"] "requirements": ["jaraco.abode==5.2.1"]
} }

View File

@@ -18,7 +18,6 @@ from homeassistant.const import (
UV_INDEX, UV_INDEX,
UnitOfIrradiance, UnitOfIrradiance,
UnitOfLength, UnitOfLength,
UnitOfPressure,
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
UnitOfTime, UnitOfTime,
@@ -280,15 +279,6 @@ 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,
@@ -298,16 +288,6 @@ 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,
@@ -315,19 +295,9 @@ 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"]},
@@ -354,7 +324,6 @@ 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

@@ -3,7 +3,6 @@
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
@@ -18,9 +17,6 @@ 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():
@@ -32,19 +28,6 @@ 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

@@ -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 AcmedaEntity(entity.Entity): class AcmedaBase(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 AcmedaEntity(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 str(self.roller.id) return self.roller.id # type: ignore[no-any-return]
@property @property
def device_id(self) -> str: def device_id(self) -> str:

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(AcmedaEntity, CoverEntity): class AcmedaCover(AcmedaBase, CoverEntity):
"""Representation of an Acmeda cover device.""" """Representation of an Acmeda cover device."""
_attr_name = None _attr_name = None

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.6"] "requirements": ["aiopulse==0.4.4"]
} }

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(AcmedaEntity, SensorEntity): class AcmedaBattery(AcmedaBase, 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 as DEVICE_TRACKER_DOMAIN, 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[DEVICE_TRACKER_DOMAIN]) scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@@ -51,6 +51,7 @@ 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."""
@@ -69,7 +70,7 @@ class ActiontecDeviceScanner(DeviceScanner):
Return boolean if scanning successful. Return boolean if scanning successful.
""" """
_LOGGER.debug("Scanning") _LOGGER.info("Scanning")
if not self.success_init: if not self.success_init:
return False return False
@@ -78,7 +79,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.debug("Scan successful") _LOGGER.info("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.debug("Adax: Failed to login to retrieve token") _LOGGER.info("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,20 +66,10 @@
} }
}, },
"services": { "services": {
"add_url": { "add_url": "mdi:link-plus",
"service": "mdi:link-plus" "remove_url": "mdi:link-off",
}, "enable_url": "mdi:link-variant",
"remove_url": { "disable_url": "mdi:link-variant-off",
"service": "mdi:link-off" "refresh": "mdi:refresh"
},
"enable_url": {
"service": "mdi:link-variant"
},
"disable_url": {
"service": "mdi:link-variant-off"
},
"refresh": {
"service": "mdi:refresh"
}
} }
} }

View File

@@ -1,6 +1,12 @@
"""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
@@ -13,38 +19,42 @@ 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.INT: pyads.PLCTYPE_INT, ADSTYPE_DINT: pyads.PLCTYPE_DINT,
AdsType.UINT: pyads.PLCTYPE_UINT, ADSTYPE_INT: pyads.PLCTYPE_INT,
AdsType.SINT: pyads.PLCTYPE_SINT, ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
AdsType.USINT: pyads.PLCTYPE_USINT, ADSTYPE_UINT: pyads.PLCTYPE_UINT,
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"
@@ -63,7 +73,16 @@ 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.Coerce(AdsType), vol.Required(CONF_ADS_TYPE): vol.In(
[
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,
} }
@@ -97,9 +116,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: str = call.data[CONF_ADS_VAR] ads_var = call.data[CONF_ADS_VAR]
ads_type: AdsType = call.data[CONF_ADS_TYPE] ads_type = call.data[CONF_ADS_TYPE]
value: int = call.data[CONF_ADS_VALUE] value = 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])
@@ -114,3 +133,181 @@ 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,9 +17,7 @@ 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 .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
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(
@@ -38,11 +36,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[DATA_ADS] ads_hub = hass.data.get(DATA_ADS)
ads_var: str = config[CONF_ADS_VAR] ads_var = config[CONF_ADS_VAR]
name: str = config[CONF_NAME] name = config[CONF_NAME]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) device_class = 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])
@@ -51,13 +49,7 @@ def setup_platform(
class AdsBinarySensor(AdsEntity, BinarySensorEntity): class AdsBinarySensor(AdsEntity, BinarySensorEntity):
"""Representation of ADS binary sensors.""" """Representation of ADS binary sensors."""
def __init__( def __init__(self, ads_hub, name, ads_var, device_class):
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

@@ -1,41 +0,0 @@
"""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,7 +11,6 @@ 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,
) )
@@ -21,9 +20,14 @@ 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 .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE from . import (
from .entity import AdsEntity CONF_ADS_VAR,
from .hub import AdsHub CONF_ADS_VAR_POSITION,
DATA_ADS,
STATE_KEY_POSITION,
STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS Cover" DEFAULT_NAME = "ADS Cover"
@@ -31,9 +35,6 @@ 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(
{ {
@@ -58,14 +59,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: str = config[CONF_ADS_VAR] ads_var_is_closed = config.get(CONF_ADS_VAR)
ads_var_position: str | None = config.get(CONF_ADS_VAR_POSITION) ads_var_position = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set: str | None = config.get(CONF_ADS_VAR_SET_POS) ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open: str | None = config.get(CONF_ADS_VAR_OPEN) ads_var_open = config.get(CONF_ADS_VAR_OPEN)
ads_var_close: str | None = config.get(CONF_ADS_VAR_CLOSE) ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop: str | None = config.get(CONF_ADS_VAR_STOP) ads_var_stop = config.get(CONF_ADS_VAR_STOP)
name: str = config[CONF_NAME] name = config[CONF_NAME]
device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
add_entities( add_entities(
[ [
@@ -89,16 +90,16 @@ class AdsCover(AdsEntity, CoverEntity):
def __init__( def __init__(
self, self,
ads_hub: AdsHub, ads_hub,
ads_var_is_closed: str, ads_var_is_closed,
ads_var_position: str | None, ads_var_position,
ads_var_pos_set: str | None, ads_var_pos_set,
ads_var_open: str | None, ads_var_open,
ads_var_close: str | None, ads_var_close,
ads_var_stop: str | None, ads_var_stop,
name: str, name,
device_class: CoverDeviceClass | None, device_class,
) -> 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

@@ -1,70 +0,0 @@
"""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

@@ -1,151 +0,0 @@
"""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,7 +1,5 @@
{ {
"services": { "services": {
"write_data_by_name": { "write_data_by_name": "mdi:pencil"
"service": "mdi:pencil"
}
} }
} }

View File

@@ -19,12 +19,14 @@ 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 .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE from . import (
from .entity import AdsEntity CONF_ADS_VAR,
from .hub import AdsHub CONF_ADS_VAR_BRIGHTNESS,
DATA_ADS,
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness" STATE_KEY_BRIGHTNESS,
STATE_KEY_BRIGHTNESS = "brightness" STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS Light" DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
@@ -43,11 +45,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[DATA_ADS] ads_hub = hass.data.get(DATA_ADS)
ads_var_enable: str = config[CONF_ADS_VAR] ads_var_enable = config[CONF_ADS_VAR]
ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS) ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS)
name: str = config[CONF_NAME] name = 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)])
@@ -55,13 +57,7 @@ def setup_platform(
class AdsLight(AdsEntity, LightEntity): class AdsLight(AdsEntity, LightEntity):
"""Representation of ADS light.""" """Representation of ADS light."""
def __init__( def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
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": ["@mrpasztoradam"], "codeowners": [],
"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

@@ -1,86 +0,0 @@
"""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,54 +5,41 @@ 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_DEVICE_CLASS, CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.const import 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_TYPEMAP, CONF_ADS_FACTOR, CONF_ADS_TYPE from .. import ads
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsType from . import (
from .entity import AdsEntity ADS_TYPEMAP,
from .hub import AdsHub CONF_ADS_FACTOR,
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=AdsType.INT): vol.All( vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In(
vol.Coerce(AdsType), [
vol.In( ads.ADSTYPE_INT,
[ ads.ADSTYPE_UINT,
AdsType.BOOL, ads.ADSTYPE_BYTE,
AdsType.BYTE, ads.ADSTYPE_DINT,
AdsType.INT, ads.ADSTYPE_UDINT,
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_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=""): cv.string,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
} }
) )
@@ -64,26 +51,15 @@ 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[DATA_ADS] ads_hub = hass.data.get(ads.DATA_ADS)
ads_var: str = config[CONF_ADS_VAR] ads_var = config[CONF_ADS_VAR]
ads_type: AdsType = config[CONF_ADS_TYPE] ads_type = config[CONF_ADS_TYPE]
name: str = config[CONF_NAME] name = config[CONF_NAME]
factor: int | None = config.get(CONF_ADS_FACTOR) unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
device_class: SensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) factor = config.get(CONF_ADS_FACTOR)
state_class: SensorStateClass | None = config.get(CONF_STATE_CLASS)
unit_of_measurement: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
entity = AdsSensor( entity = AdsSensor(ads_hub, ads_var, ads_type, name, unit_of_measurement, factor)
ads_hub,
ads_var,
ads_type,
name,
factor,
device_class,
state_class,
unit_of_measurement,
)
add_entities([entity]) add_entities([entity])
@@ -91,24 +67,12 @@ 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__( def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
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,8 +17,7 @@ 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 .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
from .entity import AdsEntity
DEFAULT_NAME = "ADS Switch" DEFAULT_NAME = "ADS Switch"
@@ -37,10 +36,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[DATA_ADS] ads_hub = hass.data.get(DATA_ADS)
name: str = config[CONF_NAME] name = config[CONF_NAME]
ads_var: str = config[CONF_ADS_VAR] ads_var = config[CONF_ADS_VAR]
add_entities([AdsSwitch(ads_hub, name, ads_var)]) add_entities([AdsSwitch(ads_hub, name, ads_var)])

View File

@@ -1,84 +0,0 @@
"""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,7 +1,5 @@
{ {
"services": { "services": {
"set_time_to": { "set_time_to": "mdi:timer-cog"
"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 import async_redact_data from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,

View File

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

View File

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

View File

@@ -13,13 +13,11 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN from .const import DOMAIN
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[AirQualityEntity]] = HassKey(DOMAIN)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
@@ -56,7 +54,7 @@ PROP_TO_ATTR: Final[dict[str, str]] = {
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the air quality component.""" """Set up the air quality component."""
component = hass.data[DATA_COMPONENT] = EntityComponent[AirQualityEntity]( component = hass.data[DOMAIN] = EntityComponent[AirQualityEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL _LOGGER, DOMAIN, hass, SCAN_INTERVAL
) )
await component.async_setup(config) await component.async_setup(config)
@@ -65,12 +63,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
return await hass.data[DATA_COMPONENT].async_setup_entry(entry) component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.data[DATA_COMPONENT].async_unload_entry(entry) component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class AirQualityEntity(Entity): class AirQualityEntity(Entity):

View File

@@ -2,14 +2,18 @@
from __future__ import annotations from __future__ import annotations
from airgradient import AirGradientClient from dataclasses import dataclass
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 .coordinator import AirGradientCoordinator from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.BUTTON, Platform.BUTTON,
@@ -21,7 +25,15 @@ PLATFORMS: list[Platform] = [
] ]
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator] @dataclass
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:
@@ -31,11 +43,27 @@ 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)
) )
coordinator = AirGradientCoordinator(hass, client) measurement_coordinator = AirGradientMeasurementCoordinator(hass, client)
config_coordinator = AirGradientConfigCoordinator(hass, client)
await coordinator.async_config_entry_first_refresh() await measurement_coordinator.async_config_entry_first_refresh()
await config_coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator device_registry = dr.async_get(hass)
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,9 +15,8 @@ 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 AirGradientConfigEntry from . import DOMAIN, AirGradientConfigEntry
from .const import DOMAIN from .coordinator import AirGradientConfigCoordinator
from .coordinator import AirGradientCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -48,8 +47,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."""
coordinator = entry.runtime_data model = entry.runtime_data.measurement.data.model
model = coordinator.data.measures.model coordinator = entry.runtime_data.config
added_entities = False added_entities = False
@@ -58,7 +57,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)] entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
@@ -68,8 +67,7 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.config.configuration_control coordinator.data.configuration_control is not ConfigurationControl.LOCAL
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -89,10 +87,11 @@ 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: AirGradientCoordinator, coordinator: AirGradientConfigCoordinator,
description: AirGradientButtonEntityDescription, description: AirGradientButtonEntityDescription,
) -> None: ) -> None:
"""Initialize airgradient button.""" """Initialize airgradient button."""

View File

@@ -92,9 +92,7 @@ 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( await self.async_set_unique_id(current_measures.serial_number)
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,35 +2,24 @@
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
from airgradient import AirGradientClient, AirGradientError, Config, Measures from airgradient import AirGradientClient, AirGradientError, Config, Measures
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER from .const import LOGGER
if TYPE_CHECKING: if TYPE_CHECKING:
from . import AirGradientConfigEntry from . import AirGradientConfigEntry
@dataclass class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
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
_current_version: str
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None: def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
@@ -44,27 +33,25 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
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_setup(self) -> None: async def _async_update_data(self) -> _DataT:
"""Set up the coordinator."""
self._current_version = (
await self.client.get_current_measures()
).firmware_version
async def _async_update_data(self) -> AirGradientData:
try: try:
measures = await self.client.get_current_measures() return await self._update_data()
config = await self.client.get_config()
except AirGradientError as error: except AirGradientError as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
if measures.firmware_version != self._current_version:
device_registry = dr.async_get(self.hass) async def _update_data(self) -> _DataT:
device_entry = device_registry.async_get_device( raise NotImplementedError
identifiers={(DOMAIN, self.serial_number)}
)
assert device_entry class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
device_registry.async_update_device( """Class to manage fetching AirGradient data."""
device_entry.id,
sw_version=measures.firmware_version, async def _update_data(self) -> Measures:
) return await self.client.get_current_measures()
self._current_version = measures.firmware_version
return AirGradientData(measures, config)
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,18 +0,0 @@
"""Diagnostics support for Airgradient."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.core import HomeAssistant
from . import AirGradientConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirGradientConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return asdict(entry.runtime_data.data)

View File

@@ -1,7 +1,5 @@
"""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
@@ -17,12 +15,6 @@ 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.9.0"], "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 AirGradientCoordinator from .coordinator import AirGradientConfigCoordinator
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."""
coordinator = entry.runtime_data model = entry.runtime_data.measurement.data.model
model = coordinator.data.measures.model coordinator = entry.runtime_data.config
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.config.configuration_control is ConfigurationControl.LOCAL coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities = [] entities = []
@@ -84,8 +84,7 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.config.configuration_control coordinator.data.configuration_control is not ConfigurationControl.LOCAL
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -105,10 +104,11 @@ 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: AirGradientCoordinator, coordinator: AirGradientConfigCoordinator,
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.config) return self.entity_description.value_fn(self.coordinator.data)
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 AirGradientCoordinator from .coordinator import AirGradientConfigCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
@@ -144,11 +144,13 @@ 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 coordinator = entry.runtime_data.config
model = coordinator.data.measures.model measurement_coordinator = entry.runtime_data.measurement
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
@@ -156,7 +158,7 @@ async def async_setup_entry(
nonlocal added_entities nonlocal added_entities
if ( if (
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
entities: list[AirGradientSelect] = [ entities: list[AirGradientSelect] = [
@@ -177,8 +179,7 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
added_entities = True added_entities = True
elif ( elif (
coordinator.data.config.configuration_control coordinator.data.configuration_control is not ConfigurationControl.LOCAL
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -200,10 +201,11 @@ 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: AirGradientCoordinator, coordinator: AirGradientConfigCoordinator,
description: AirGradientSelectEntityDescription, description: AirGradientSelectEntityDescription,
) -> None: ) -> None:
"""Initialize AirGradient select.""" """Initialize AirGradient select."""
@@ -214,7 +216,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.config) return self.entity_description.value_fn(self.coordinator.data)
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 AirGradientCoordinator from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
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 coordinator = entry.runtime_data.measurement
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.measures) is None: if description.value_fn(coordinator.data) is None:
not_setup.add(description) not_setup.add(description)
else: else:
sensors.append(AirGradientMeasurementSensor(coordinator, description)) sensors.append(AirGradientMeasurementSensor(coordinator, description))
@@ -248,65 +248,64 @@ async def async_setup_entry(
add_entities() add_entities()
entities = [ entities = [
AirGradientConfigSensor(coordinator, description) AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_SENSOR_TYPES for description in CONFIG_SENSOR_TYPES
] ]
if "L" in coordinator.data.measures.model: if "L" in coordinator.data.model:
entities.extend( entities.extend(
AirGradientConfigSensor(coordinator, description) AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_LED_BAR_SENSOR_TYPES for description in CONFIG_LED_BAR_SENSOR_TYPES
) )
if "I" in coordinator.data.measures.model: if "I" in coordinator.data.model:
entities.extend( entities.extend(
AirGradientConfigSensor(coordinator, description) AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_DISPLAY_SENSOR_TYPES for description in CONFIG_DISPLAY_SENSOR_TYPES
) )
async_add_entities(entities) async_add_entities(entities)
class AirGradientSensor(AirGradientEntity, SensorEntity): class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity):
"""Defines an AirGradient sensor.""" """Defines an AirGradient sensor."""
entity_description: AirGradientMeasurementSensorEntityDescription
coordinator: AirGradientMeasurementCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientCoordinator, coordinator: AirGradientMeasurementCoordinator,
description: SensorEntityDescription, description: AirGradientMeasurementSensorEntityDescription,
) -> 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.measures) return self.entity_description.value_fn(self.coordinator.data)
class AirGradientConfigSensor(AirGradientSensor): class AirGradientConfigSensor(AirGradientEntity, SensorEntity):
"""Defines an AirGradient sensor.""" """Defines an AirGradient sensor."""
entity_description: AirGradientConfigSensorEntityDescription entity_description: AirGradientConfigSensorEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__( def __init__(
self, self,
coordinator: AirGradientCoordinator, coordinator: AirGradientConfigCoordinator,
description: AirGradientConfigSensorEntityDescription, description: AirGradientConfigSensorEntityDescription,
) -> None: ) -> None:
"""Initialize airgradient sensor.""" """Initialize airgradient sensor."""
super().__init__(coordinator, description) super().__init__(coordinator)
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.config.configuration_control coordinator.data.configuration_control is not ConfigurationControl.LOCAL
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.config) return self.entity_description.value_fn(self.coordinator.data)

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 AirGradientCoordinator from .coordinator import AirGradientConfigCoordinator
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 coordinator = entry.runtime_data.config
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.config.configuration_control is ConfigurationControl.LOCAL coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities and not added_entities
): ):
async_add_entities( async_add_entities(
@@ -63,8 +63,7 @@ async def async_setup_entry(
) )
added_entities = True added_entities = True
elif ( elif (
coordinator.data.config.configuration_control coordinator.data.configuration_control is not ConfigurationControl.LOCAL
is not ConfigurationControl.LOCAL
and added_entities and added_entities
): ):
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@@ -83,10 +82,11 @@ 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: AirGradientCoordinator, coordinator: AirGradientConfigCoordinator,
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.config) return self.entity_description.value_fn(self.coordinator.data)
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

@@ -7,7 +7,7 @@ from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
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 . import AirGradientConfigEntry, AirGradientCoordinator from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity from .entity import AirGradientEntity
SCAN_INTERVAL = timedelta(hours=1) SCAN_INTERVAL = timedelta(hours=1)
@@ -20,17 +20,18 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Airgradient update platform.""" """Set up Airgradient update platform."""
coordinator = config_entry.runtime_data data = config_entry.runtime_data
async_add_entities([AirGradientUpdate(coordinator)], True) async_add_entities([AirGradientUpdate(data.measurement)], True)
class AirGradientUpdate(AirGradientEntity, UpdateEntity): class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update.""" """Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
coordinator: AirGradientMeasurementCoordinator
def __init__(self, coordinator: AirGradientCoordinator) -> None: def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-update" self._attr_unique_id = f"{coordinator.serial_number}-update"
@@ -43,7 +44,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
@property @property
def installed_version(self) -> str: def installed_version(self) -> str:
"""Return the installed version of the entity.""" """Return the installed version of the entity."""
return self.coordinator.data.measures.firmware_version return self.coordinator.data.firmware_version
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the entity.""" """Update the entity."""

View File

@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN # noqa: F401
from .coordinator import AirNowDataUpdateCoordinator from .coordinator import AirNowDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@@ -14,32 +14,10 @@ ATTR_API_POLLUTANT = "Pollutant"
ATTR_API_REPORT_DATE = "DateObserved" ATTR_API_REPORT_DATE = "DateObserved"
ATTR_API_REPORT_HOUR = "HourObserved" ATTR_API_REPORT_HOUR = "HourObserved"
ATTR_API_REPORT_TZ = "LocalTimeZone" ATTR_API_REPORT_TZ = "LocalTimeZone"
ATTR_API_REPORT_TZINFO = "LocalTimeZoneInfo"
ATTR_API_STATE = "StateCode" ATTR_API_STATE = "StateCode"
ATTR_API_STATION = "ReportingArea" ATTR_API_STATION = "ReportingArea"
ATTR_API_STATION_LATITUDE = "Latitude" ATTR_API_STATION_LATITUDE = "Latitude"
ATTR_API_STATION_LONGITUDE = "Longitude" ATTR_API_STATION_LONGITUDE = "Longitude"
DEFAULT_NAME = "AirNow" DEFAULT_NAME = "AirNow"
DOMAIN = "airnow" DOMAIN = "airnow"
SECONDS_PER_HOUR = 3600
# AirNow seems to only use standard time zones,
# but we include daylight savings for completeness/futureproofing.
US_TZ_OFFSETS = {
"HST": -10 * SECONDS_PER_HOUR,
"HDT": -9 * SECONDS_PER_HOUR,
# AirNow returns AKT instead of AKST or AKDT, use standard
"AKT": -9 * SECONDS_PER_HOUR,
"AKST": -9 * SECONDS_PER_HOUR,
"AKDT": -8 * SECONDS_PER_HOUR,
"PST": -8 * SECONDS_PER_HOUR,
"PDT": -7 * SECONDS_PER_HOUR,
"MST": -7 * SECONDS_PER_HOUR,
"MDT": -6 * SECONDS_PER_HOUR,
"CST": -6 * SECONDS_PER_HOUR,
"CDT": -5 * SECONDS_PER_HOUR,
"EST": -5 * SECONDS_PER_HOUR,
"EDT": -4 * SECONDS_PER_HOUR,
"AST": -4 * SECONDS_PER_HOUR,
"ADT": -3 * SECONDS_PER_HOUR,
}

View File

@@ -12,6 +12,7 @@ from pyairnow.errors import AirNowError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTR_API_AQI, ATTR_API_AQI,
@@ -26,6 +27,7 @@ from .const import (
ATTR_API_REPORT_DATE, ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR, ATTR_API_REPORT_HOUR,
ATTR_API_REPORT_TZ, ATTR_API_REPORT_TZ,
ATTR_API_REPORT_TZINFO,
ATTR_API_STATE, ATTR_API_STATE,
ATTR_API_STATION, ATTR_API_STATION,
ATTR_API_STATION_LATITUDE, ATTR_API_STATION_LATITUDE,
@@ -96,7 +98,9 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Copy Report Details # Copy Report Details
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE] data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR] data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ] data[ATTR_API_REPORT_TZINFO] = await dt_util.async_get_time_zone(
obv[ATTR_API_REPORT_TZ]
)
# Copy Station Details # Copy Station Details
data[ATTR_API_STATE] = obv[ATTR_API_STATE] data[ATTR_API_STATE] = obv[ATTR_API_STATE]

View File

@@ -4,10 +4,9 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Any from typing import Any
from dateutil import parser
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@@ -35,13 +34,12 @@ from .const import (
ATTR_API_PM25, ATTR_API_PM25,
ATTR_API_REPORT_DATE, ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR, ATTR_API_REPORT_HOUR,
ATTR_API_REPORT_TZ, ATTR_API_REPORT_TZINFO,
ATTR_API_STATION, ATTR_API_STATION,
ATTR_API_STATION_LATITUDE, ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE, ATTR_API_STATION_LONGITUDE,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
US_TZ_OFFSETS,
) )
ATTRIBUTION = "Data provided by AirNow" ATTRIBUTION = "Data provided by AirNow"
@@ -71,18 +69,6 @@ def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
return {} return {}
def aqi_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
"""Process extra attributes for main AQI sensor."""
return {
ATTR_DESCR: data[ATTR_API_AQI_DESCRIPTION],
ATTR_LEVEL: data[ATTR_API_AQI_LEVEL],
ATTR_TIME: parser.parse(
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}:00 {data[ATTR_API_REPORT_TZ]}",
tzinfos=US_TZ_OFFSETS,
).isoformat(),
}
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription( AirNowEntityDescription(
key=ATTR_API_AQI, key=ATTR_API_AQI,
@@ -90,7 +76,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
value_fn=lambda data: data.get(ATTR_API_AQI), value_fn=lambda data: data.get(ATTR_API_AQI),
extra_state_attributes_fn=aqi_extra_attrs, extra_state_attributes_fn=lambda data: {
ATTR_DESCR: data[ATTR_API_AQI_DESCRIPTION],
ATTR_LEVEL: data[ATTR_API_AQI_LEVEL],
ATTR_TIME: datetime.strptime(
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}",
"%Y-%m-%d %H",
)
.replace(tzinfo=data[ATTR_API_REPORT_TZINFO])
.isoformat(),
},
), ),
AirNowEntityDescription( AirNowEntityDescription(
key=ATTR_API_PM10, key=ATTR_API_PM10,

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.1"] "requirements": ["airthings-ble==0.9.0"]
} }

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(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp) await self._control(temp=temp)
class Airtouch5Zone(Airtouch5ClimateEntity): class Airtouch5Zone(Airtouch5ClimateEntity):

View File

@@ -34,8 +34,13 @@ from homeassistant.helpers import (
device_registry as dr, device_registry as dr,
entity_registry as er, entity_registry as er,
) )
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import ( from .const import (
CONF_CITY, CONF_CITY,
@@ -48,8 +53,6 @@ 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"
@@ -88,9 +91,10 @@ 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 [
entry.runtime_data coordinator
for entry in hass.config_entries.async_entries(DOMAIN) for entry_id, coordinator in hass.data[DOMAIN].items()
if entry.data.get(CONF_API_KEY) == api_key and hasattr(entry, "runtime_data") if (entry := hass.config_entries.async_get_entry(entry_id))
and entry.data.get(CONF_API_KEY) == api_key
] ]
@@ -168,7 +172,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: AirVisualConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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;
@@ -216,7 +220,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
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()
entry.runtime_data = coordinator hass.data.setdefault(DOMAIN, {})
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])
@@ -226,7 +231,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
return True return True
async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate an old config entry.""" """Migrate an old config entry."""
version = entry.version version = entry.version
@@ -383,18 +388,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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 and CONF_API_KEY in entry.data: if unload_ok:
# Re-calculate the update interval period for any remaining consumers of hass.data[DOMAIN].pop(entry.entry_id)
# this API key: if CONF_API_KEY in entry.data:
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) # Re-calculate the update interval period for any remaining consumers of
# 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: AirVisualConfigEntry) -> None: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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)
class AirVisualEntity(CoordinatorEntity):
"""Define a generic AirVisual entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: EntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_extra_state_attributes = {}
self._entry = entry
self.entity_description = description
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
@callback
def update() -> None:
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
self.async_on_remove(self.coordinator.async_add_listener(update))
self.update_from_latest_data()
@callback
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError

View File

@@ -5,6 +5,7 @@ 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,
@@ -14,9 +15,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 . import AirVisualConfigEntry from .const import CONF_CITY, DOMAIN
from .const import CONF_CITY
CONF_COORDINATES = "coordinates" CONF_COORDINATES = "coordinates"
CONF_TITLE = "title" CONF_TITLE = "title"
@@ -36,10 +37,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirVisualConfigEntry hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator = entry.runtime_data coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return { return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT), "entry": async_redact_data(entry.as_dict(), TO_REDACT),

View File

@@ -1,47 +0,0 @@
"""The AirVisual component."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
class AirVisualEntity(CoordinatorEntity):
"""Define a generic AirVisual entity."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: EntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_extra_state_attributes = {}
self._entry = entry
self.entity_description = description
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
@callback
def update() -> None:
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
self.async_on_remove(self.coordinator.async_add_listener(update))
self.update_from_latest_data()
@callback
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError

View File

@@ -26,9 +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 AirVisualConfigEntry from . import AirVisualEntity
from .const import CONF_CITY from .const import CONF_CITY, DOMAIN
from .entity import AirVisualEntity
ATTR_CITY = "city" ATTR_CITY = "city"
ATTR_COUNTRY = "country" ATTR_COUNTRY = "country"
@@ -106,12 +105,10 @@ POLLUTANT_UNITS = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
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 = entry.runtime_data coordinator = hass.data[DOMAIN][entry.entry_id]
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

@@ -24,9 +24,15 @@ from homeassistant.const import (
) )
from homeassistant.core import Event, HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import LOGGER from .const import DOMAIN, LOGGER
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
@@ -114,3 +120,28 @@ async def async_unload_entry(
await entry.runtime_data.node.async_disconnect() await entry.runtime_data.node.async_disconnect()
return unload_ok return unload_ok
class AirVisualProEntity(CoordinatorEntity):
"""Define a generic AirVisual Pro entity."""
def __init__(
self, coordinator: DataUpdateCoordinator, description: EntityDescription
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}"
self.entity_description = description
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
manufacturer="AirVisual",
model=self.coordinator.data["status"]["model"],
name=self.coordinator.data["settings"]["node_name"],
hw_version=self.coordinator.data["status"]["system_version"],
sw_version=self.coordinator.data["status"]["app_version"],
)

View File

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

@@ -1,37 +0,0 @@
"""The AirVisual Pro integration."""
from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import DOMAIN
class AirVisualProEntity(CoordinatorEntity):
"""Define a generic AirVisual Pro entity."""
def __init__(
self, coordinator: DataUpdateCoordinator, description: EntityDescription
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}"
self.entity_description = description
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
manufacturer="AirVisual",
model=self.coordinator.data["status"]["model"],
name=self.coordinator.data["settings"]["node_name"],
hw_version=self.coordinator.data["status"]["system_version"],
sw_version=self.coordinator.data["status"]["app_version"],
)

View File

@@ -22,8 +22,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirVisualProConfigEntry from . import AirVisualProConfigEntry, AirVisualProEntity
from .entity import AirVisualProEntity
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)

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 import async_redact_data from homeassistant.components.diagnostics.util 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.9.3"] "requirements": ["aioairzone==0.8.2"]
} }

View File

@@ -2,21 +2,16 @@
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, OperationMode, SleepTimeout from aioairzone.common import GrilleAngle, 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,
) )
@@ -38,9 +33,6 @@ 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]] = {
@@ -50,15 +42,6 @@ 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,
@@ -67,26 +50,6 @@ 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,
@@ -132,20 +95,7 @@ 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:
entities: list[AirzoneZoneSelect] = [ async_add_entities(
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,
@@ -156,8 +106,7 @@ 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))
@@ -204,11 +153,6 @@ 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,17 +52,6 @@
"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

@@ -21,7 +21,7 @@ from aioairzone_cloud.const import (
RAW_WEBSERVERS, RAW_WEBSERVERS,
) )
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics.util 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.5"] "requirements": ["aioairzone-cloud==0.6.2"]
} }

View File

@@ -12,16 +12,7 @@ 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,
@@ -41,9 +32,7 @@ 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
@@ -59,78 +48,6 @@ 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,

View File

@@ -45,33 +45,6 @@
"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

@@ -33,7 +33,6 @@ from homeassistant.helpers.deprecation import (
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401 from .const import ( # noqa: F401
_DEPRECATED_FORMAT_NUMBER, _DEPRECATED_FORMAT_NUMBER,
@@ -53,7 +52,6 @@ from .const import ( # noqa: F401
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[AlarmControlPanelEntity]] = HassKey(DOMAIN)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
@@ -71,7 +69,7 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for sensors.""" """Track states and offer events for sensors."""
component = hass.data[DATA_COMPONENT] = EntityComponent[AlarmControlPanelEntity]( component = hass.data[DOMAIN] = EntityComponent[AlarmControlPanelEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL _LOGGER, DOMAIN, hass, SCAN_INTERVAL
) )
@@ -124,12 +122,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
return await hass.data[DATA_COMPONENT].async_setup_entry(entry) component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.data[DATA_COMPONENT].async_unload_entry(entry) component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True): class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):

View File

@@ -15,26 +15,12 @@
} }
}, },
"services": { "services": {
"alarm_arm_away": { "alarm_arm_away": "mdi:shield-lock",
"service": "mdi:shield-lock" "alarm_arm_home": "mdi:shield-home",
}, "alarm_arm_night": "mdi:shield-moon",
"alarm_arm_home": { "alarm_arm_custom_bypass": "mdi:security",
"service": "mdi:shield-home" "alarm_disarm": "mdi:shield-off",
}, "alarm_trigger": "mdi:bell-ring",
"alarm_arm_night": { "alarm_arm_vacation": "mdi:shield-airplane"
"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,11 +7,7 @@
} }
}, },
"services": { "services": {
"alarm_keypress": { "alarm_keypress": "mdi:dialpad",
"service": "mdi:dialpad" "alarm_toggle_chime": "mdi:abc"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
}
} }
} }

View File

@@ -2,8 +2,18 @@
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,
@@ -12,12 +22,22 @@ 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 HomeAssistant from homeassistant.core import Event, EventStateChangedData, HassJob, 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,
@@ -32,7 +52,6 @@ from .const import (
DOMAIN, DOMAIN,
LOGGER, LOGGER,
) )
from .entity import AlertEntity
ALERT_SCHEMA = vol.Schema( ALERT_SCHEMA = vol.Schema(
{ {
@@ -64,9 +83,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[AlertEntity](LOGGER, DOMAIN, hass) component = EntityComponent[Alert](LOGGER, DOMAIN, hass)
entities: list[AlertEntity] = [] entities: list[Alert] = []
for object_id, cfg in config[DOMAIN].items(): for object_id, cfg in config[DOMAIN].items():
if not cfg: if not cfg:
@@ -85,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
data = cfg.get(CONF_DATA) data = cfg.get(CONF_DATA)
entities.append( entities.append(
AlertEntity( Alert(
hass, hass,
object_id, object_id,
name, name,
@@ -112,3 +131,183 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
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
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,206 +0,0 @@
"""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,13 +1,7 @@
{ {
"services": { "services": {
"toggle": { "toggle": "mdi:bell-ring",
"service": "mdi:bell-ring" "turn_off": "mdi:bell-off",
}, "turn_on": "mdi:bell-alert"
"turn_off": {
"service": "mdi:bell-off"
},
"turn_on": {
"service": "mdi:bell-alert"
}
} }
} }

View File

@@ -29,7 +29,6 @@ from homeassistant.components.alarm_control_panel import (
CodeFormat, CodeFormat,
) )
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from homeassistant.components.lock import LockState
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE_FORMAT, ATTR_CODE_FORMAT,
ATTR_SUPPORTED_FEATURES, ATTR_SUPPORTED_FEATURES,
@@ -41,12 +40,16 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT,
STATE_IDLE, STATE_IDLE,
STATE_LOCKED,
STATE_LOCKING,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, STATE_PLAYING,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKED,
STATE_UNLOCKING,
UnitOfLength, UnitOfLength,
UnitOfMass, UnitOfMass,
UnitOfTemperature, UnitOfTemperature,
@@ -497,10 +500,10 @@ class AlexaLockController(AlexaCapability):
raise UnsupportedProperty(name) raise UnsupportedProperty(name)
# If its unlocking its still locked and not unlocked yet # If its unlocking its still locked and not unlocked yet
if self.entity.state in (LockState.UNLOCKING, LockState.LOCKED): if self.entity.state in (STATE_UNLOCKING, STATE_LOCKED):
return "LOCKED" return "LOCKED"
# If its locking its still unlocked and not locked yet # If its locking its still unlocked and not locked yet
if self.entity.state in (LockState.LOCKING, LockState.UNLOCKED): if self.entity.state in (STATE_LOCKING, STATE_UNLOCKED):
return "UNLOCKED" return "UNLOCKED"
return "JAMMED" return "JAMMED"

View File

@@ -661,12 +661,9 @@ 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)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) yield AlexaModeController(
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or [] self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
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

@@ -2,7 +2,6 @@
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
@@ -115,8 +114,6 @@ 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", []):
@@ -127,12 +124,8 @@ 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( return AmazonPollyProvider(polly_client, config, supported_languages, all_voices)
polly_client, config, supported_languages, all_voices, all_engines
)
class AmazonPollyProvider(Provider): class AmazonPollyProvider(Provider):
@@ -144,16 +137,13 @@ 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
@@ -169,12 +159,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, CONF_ENGINE: self.default_engine} return {CONF_VOICE: self.default_voice}
@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, CONF_ENGINE] return [CONF_VOICE]
def get_tts_audio( def get_tts_audio(
self, self,
@@ -189,14 +179,9 @@ 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=engine, Engine=self.config[CONF_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,13 +7,11 @@ 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, PLATFORMS from .const import CONF_SITE_ID, DOMAIN, 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)
@@ -21,11 +19,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> boo
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()
entry.runtime_data = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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: AmberConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = 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 . import AmberConfigEntry from .const import ATTRIBUTION, DOMAIN
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: AmberConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
coordinator = entry.runtime_data coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
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 . import AmberConfigEntry from .const import ATTRIBUTION, DOMAIN
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: AmberConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
coordinator = entry.runtime_data coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
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,30 +8,28 @@ 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( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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()
entry.runtime_data = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok := 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.08.0"] "requirements": ["aioambient==2024.01.0"]
} }

View File

@@ -10,6 +10,7 @@ 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,
@@ -28,7 +29,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 . import AmbientNetworkConfigEntry from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator from .coordinator import AmbientNetworkDataUpdateCoordinator
from .entity import AmbientNetworkEntity from .entity import AmbientNetworkEntity
@@ -270,12 +271,12 @@ SENSOR_DESCRIPTIONS = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AmbientNetworkConfigEntry, entry: ConfigEntry,
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 = entry.runtime_data coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if coordinator.config_entry is not None: if coordinator.config_entry is not None:
async_add_entities( async_add_entities(
AmbientNetworkSensor( AmbientNetworkSensor(

View File

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

View File

@@ -1,37 +1,15 @@
{ {
"services": { "services": {
"enable_recording": { "enable_recording": "mdi:record-rec",
"service": "mdi:record-rec" "disable_recording": "mdi:stop",
}, "enable_audio": "mdi:volume-high",
"disable_recording": { "disable_audio": "mdi:volume-off",
"service": "mdi:stop" "enable_motion_recording": "mdi:motion-sensor",
}, "disable_motion_recording": "mdi:motion-sensor-off",
"enable_audio": { "goto_preset": "mdi:pan",
"service": "mdi:volume-high" "set_color_bw": "mdi:palette",
}, "start_tour": "mdi:panorama",
"disable_audio": { "stop_tour": "mdi:panorama-outline",
"service": "mdi:volume-off" "ptz_control": "mdi:pan"
},
"enable_motion_recording": {
"service": "mdi:motion-sensor"
},
"disable_motion_recording": {
"service": "mdi:motion-sensor-off"
},
"goto_preset": {
"service": "mdi:pan"
},
"set_color_bw": {
"service": "mdi:palette"
},
"start_tour": {
"service": "mdi:panorama"
},
"stop_tour": {
"service": "mdi:panorama-outline"
},
"ptz_control": {
"service": "mdi:pan"
}
} }
} }

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