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
4473 changed files with 44719 additions and 166867 deletions

View File

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

View File

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

View File

@@ -39,8 +39,8 @@ on:
env:
CACHE_VERSION: 10
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2024.10"
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.9"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
@@ -234,7 +234,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -252,7 +252,7 @@ jobs:
python -m venv venv
. venv/bin/activate
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)"
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -279,7 +279,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -319,7 +319,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -359,7 +359,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -429,32 +429,17 @@ jobs:
. venv/bin/activate
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
lint-hadolint:
name: Check ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- info
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
strategy:
fail-fast: false
matrix:
file:
- Dockerfile
- Dockerfile.dev
- script/hassfest/docker/Dockerfile
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Register hadolint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
- name: Check ${{ matrix.file }}
uses: docker://hadolint/hadolint:v2.12.0
- name: Check Dockerfile
uses: docker://hadolint/hadolint:v1.18.2
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:
name: Prepare dependencies
@@ -469,14 +454,14 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Generate partial uv restore key
id: generate-uv-key
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 "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
@@ -525,7 +510,7 @@ jobs:
python -m venv venv
. venv/bin/activate
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 -r requirements.txt
python -m script.gen_requirements_all ci
@@ -553,7 +538,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -586,7 +571,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -620,7 +605,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -638,7 +623,7 @@ jobs:
. venv/bin/activate
pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: licenses
path: licenses.json
@@ -663,7 +648,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -710,7 +695,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -755,7 +740,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -830,7 +815,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -848,7 +833,7 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -894,7 +879,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -949,14 +934,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1014,7 +999,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1075,7 +1060,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1083,7 +1068,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1140,7 +1125,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1202,7 +1187,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1210,7 +1195,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1286,7 +1271,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1344,14 +1329,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.2.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -46,7 +46,7 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
pip install "$(grep '^uv' < requirements_test.txt)"
uv pip install -r requirements.txt
- name: Get information
@@ -82,15 +82,14 @@ jobs:
) > .env_file
- name: Upload env_file
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: env_file
path: ./.env_file
include-hidden-files: true
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -102,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -131,12 +130,6 @@ jobs:
with:
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
uses: home-assistant/wheels@2024.07.1
with:
@@ -146,7 +139,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
skip-binary: aiohttp;multidict;yarl
skip-binary: aiohttp
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt"
@@ -180,18 +173,6 @@ jobs:
with:
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
run: |
# We split requirements all into multiple files.
@@ -212,6 +193,15 @@ jobs:
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> 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)
uses: home-assistant/wheels@2024.07.1
with:
@@ -221,7 +211,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
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"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt"
@@ -236,7 +226,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
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"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
@@ -250,7 +240,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
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"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
@@ -264,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
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"
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"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.6
rev: v0.6.2
hooks:
- id: ruff
args:
@@ -83,7 +83,7 @@ repos:
pass_filenames: false
language: script
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
name: hassfest-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.aseko_pool_live.*
homeassistant.components.assist_pipeline.*
homeassistant.components.assist_satellite.*
homeassistant.components.asuswrt.*
homeassistant.components.autarco.*
homeassistant.components.auth.*
@@ -111,7 +110,6 @@ homeassistant.components.bitcoin.*
homeassistant.components.blockchain.*
homeassistant.components.blue_current.*
homeassistant.components.blueprint.*
homeassistant.components.bluesound.*
homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
@@ -141,7 +139,6 @@ homeassistant.components.cpuspeed.*
homeassistant.components.crownstone.*
homeassistant.components.date.*
homeassistant.components.datetime.*
homeassistant.components.deako.*
homeassistant.components.deconz.*
homeassistant.components.default_config.*
homeassistant.components.demo.*
@@ -211,8 +208,6 @@ homeassistant.components.glances.*
homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.*
homeassistant.components.google_cloud.*
homeassistant.components.google_photos.*
homeassistant.components.google_sheets.*
homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
@@ -283,7 +278,6 @@ homeassistant.components.lawn_mower.*
homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
@@ -300,6 +294,7 @@ homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.*
homeassistant.components.manual.*
homeassistant.components.map.*
homeassistant.components.mastodon.*
@@ -316,7 +311,6 @@ homeassistant.components.minecraft_server.*
homeassistant.components.mjpeg.*
homeassistant.components.modbus.*
homeassistant.components.modem_callerid.*
homeassistant.components.mold_indicator.*
homeassistant.components.monzo.*
homeassistant.components.moon.*
homeassistant.components.mopeka.*
@@ -343,7 +337,6 @@ homeassistant.components.nut.*
homeassistant.components.onboarding.*
homeassistant.components.oncue.*
homeassistant.components.onewire.*
homeassistant.components.onkyo.*
homeassistant.components.open_meteo.*
homeassistant.components.openexchangerates.*
homeassistant.components.opensky.*
@@ -403,7 +396,6 @@ homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
homeassistant.components.sensor.*
homeassistant.components.sensoterra.*
homeassistant.components.senz.*
homeassistant.components.sfr_box.*
homeassistant.components.shelly.*
@@ -415,13 +407,10 @@ homeassistant.components.skybell.*
homeassistant.components.slack.*
homeassistant.components.sleepiq.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.*
homeassistant.components.sql.*
homeassistant.components.squeezebox.*
homeassistant.components.ssdp.*
homeassistant.components.starlink.*
homeassistant.components.statistics.*
@@ -481,7 +470,6 @@ homeassistant.components.update.*
homeassistant.components.uptime.*
homeassistant.components.uptimerobot.*
homeassistant.components.usb.*
homeassistant.components.uvc.*
homeassistant.components.vacuum.*
homeassistant.components.vallox.*
homeassistant.components.valve.*

View File

@@ -48,7 +48,6 @@ build.json @home-assistant/supervisor
/tests/components/adax/ @danielhiversen
/homeassistant/components/adguard/ @frenck
/tests/components/adguard/ @frenck
/homeassistant/components/ads/ @mrpasztoradam
/homeassistant/components/advantage_air/ @Bre77
/tests/components/advantage_air/ @Bre77
/homeassistant/components/aemet/ @Noltari
@@ -144,8 +143,6 @@ build.json @home-assistant/supervisor
/tests/components/aseko_pool_live/ @milanmeu
/homeassistant/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
/tests/components/asuswrt/ @kennedyshead @ollo69
/homeassistant/components/atag/ @MatsNL
@@ -231,16 +228,14 @@ build.json @home-assistant/supervisor
/homeassistant/components/bsblan/ @liudger
/tests/components/bsblan/ @liudger
/homeassistant/components/bt_smarthub/ @typhoon2099
/homeassistant/components/bthome/ @Ernst79 @thecode
/tests/components/bthome/ @Ernst79 @thecode
/homeassistant/components/bthome/ @Ernst79
/tests/components/bthome/ @Ernst79
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
/tests/components/buienradar/ @mjj4791 @ties @Robbie1221
/homeassistant/components/button/ @home-assistant/core
/tests/components/button/ @home-assistant/core
/homeassistant/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
/tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery
@@ -299,8 +294,6 @@ build.json @home-assistant/supervisor
/tests/components/date/ @home-assistant/core
/homeassistant/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
/tests/components/debugpy/ @frenck
/homeassistant/components/deconz/ @Kane610
@@ -360,8 +353,6 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221
/homeassistant/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
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
@@ -556,14 +547,11 @@ build.json @home-assistant/supervisor
/tests/components/google_assistant/ @home-assistant/cloud
/homeassistant/components/google_assistant_sdk/ @tronikos
/tests/components/google_assistant_sdk/ @tronikos
/homeassistant/components/google_cloud/ @lufton @tronikos
/tests/components/google_cloud/ @lufton @tronikos
/homeassistant/components/google_cloud/ @lufton
/homeassistant/components/google_generative_ai_conversation/ @tronikos
/tests/components/google_generative_ai_conversation/ @tronikos
/homeassistant/components/google_mail/ @tkdrob
/tests/components/google_mail/ @tkdrob
/homeassistant/components/google_photos/ @allenporter
/tests/components/google_photos/ @allenporter
/homeassistant/components/google_sheets/ @tkdrob
/tests/components/google_sheets/ @tkdrob
/homeassistant/components/google_tasks/ @allenporter
@@ -641,8 +629,6 @@ build.json @home-assistant/supervisor
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle
@@ -721,8 +707,8 @@ build.json @home-assistant/supervisor
/tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iotty/ @pburgio @shapournemati-iotty
/tests/components/iotty/ @pburgio @shapournemati-iotty
/homeassistant/components/iotty/ @pburgio
/tests/components/iotty/ @pburgio
/homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes
@@ -735,8 +721,6 @@ build.json @home-assistant/supervisor
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco
/tests/components/isal/ @bdraco
/homeassistant/components/iskra/ @iskramis
/tests/components/iskra/ @iskramis
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/israel_rail/ @shaiu
@@ -813,8 +797,6 @@ build.json @home-assistant/supervisor
/tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco
/homeassistant/components/lektrico/ @lektrico
/tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob
@@ -861,8 +843,8 @@ build.json @home-assistant/supervisor
/tests/components/lupusec/ @majuss @suaveolent
/homeassistant/components/lutron/ @cdheiser @wilburCForce
/tests/components/lutron/ @cdheiser @wilburCForce
/homeassistant/components/lutron_caseta/ @swails @danaues @eclair4151
/tests/components/lutron_caseta/ @swails @danaues @eclair4151
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea
@@ -925,8 +907,6 @@ build.json @home-assistant/supervisor
/tests/components/modern_forms/ @wonderslug
/homeassistant/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
/tests/components/monoprice/ @etsinko @OnFreund
/homeassistant/components/monzo/ @jakemartin-icl
@@ -1024,8 +1004,6 @@ build.json @home-assistant/supervisor
/tests/components/nut/ @bdraco @ollo69 @pestevez
/homeassistant/components/nws/ @MatthewFlamm @kamiyo
/tests/components/nws/ @MatthewFlamm @kamiyo
/homeassistant/components/nyt_games/ @joostlek
/tests/components/nyt_games/ @joostlek
/homeassistant/components/nzbget/ @chriscla
/tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney
@@ -1104,6 +1082,8 @@ build.json @home-assistant/supervisor
/tests/components/pi_hole/ @shenxn
/homeassistant/components/picnic/ @corneyl
/tests/components/picnic/ @corneyl
/homeassistant/components/pilight/ @trekky12
/tests/components/pilight/ @trekky12
/homeassistant/components/ping/ @jpbede
/tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan
@@ -1133,8 +1113,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/prusalink/ @balloob
/tests/components/prusalink/ @balloob
/homeassistant/components/prusalink/ @balloob @Skaronator
/tests/components/prusalink/ @balloob @Skaronator
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pure_energie/ @klaasnicolaas
@@ -1295,8 +1275,6 @@ build.json @home-assistant/supervisor
/tests/components/sensorpro/ @bdraco
/homeassistant/components/sensorpush/ @bdraco
/tests/components/sensorpush/ @bdraco
/homeassistant/components/sensoterra/ @markruys
/tests/components/sensoterra/ @markruys
/homeassistant/components/sentry/ @dcramer @frenck
/tests/components/sentry/ @dcramer @frenck
/homeassistant/components/senz/ @milanmeu
@@ -1434,10 +1412,10 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
/tests/components/switcher_kis/ @thecode @YogevBokobza
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
/homeassistant/components/syncthing/ @zhulik
/tests/components/syncthing/ @zhulik
@@ -1515,8 +1493,6 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek
/homeassistant/components/touchline_sl/ @jnsgruk
/tests/components/touchline_sl/ @jnsgruk
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
/homeassistant/components/tplink_omada/ @MarkGodwin
@@ -1541,8 +1517,6 @@ build.json @home-assistant/supervisor
/tests/components/transmission/ @engrbm87 @JPHutchins
/homeassistant/components/trend/ @jpbede
/tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
@@ -1647,8 +1621,6 @@ build.json @home-assistant/supervisor
/tests/components/webostv/ @thecode
/homeassistant/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
/tests/components/wemo/ @esev
/homeassistant/components/whirlpool/ @abmantis @mkmer
@@ -1666,8 +1638,6 @@ build.json @home-assistant/supervisor
/tests/components/wiz/ @sbidy
/homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck
/homeassistant/components/wmspro/ @mback2k
/tests/components/wmspro/ @mback2k
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
/tests/components/wolflink/ @adamkrol93 @mtielen
/homeassistant/components/workday/ @fabaff @gjohansson-ST
@@ -1688,8 +1658,6 @@ build.json @home-assistant/supervisor
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
/homeassistant/components/xiaomi_tv/ @simse
/homeassistant/components/xmpp/ @fabaff @flowolf
/homeassistant/components/yale/ @bdraco
/tests/components/yale/ @bdraco
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
/tests/components/yale_smart_alarm/ @gjohansson-ST
/homeassistant/components/yalexs_ble/ @bdraco

View File

@@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.4.15
RUN pip3 install uv==0.2.27
WORKDIR /usr/src
@@ -29,9 +29,15 @@ RUN \
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
uv pip install homeassistant/home_assistant_*.whl; \
fi \
&& uv pip install \
--no-build \
-r homeassistant/requirements_all.txt
&& if [ "${BUILD_ARCH}" = "i386" ]; then \
linux32 uv pip install \
--no-build \
-r homeassistant/requirements_all.txt; \
else \
uv pip install \
--no-build \
-r homeassistant/requirements_all.txt; \
fi
## Setup Home Assistant Core
COPY . homeassistant/

View File

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

View File

@@ -127,11 +127,7 @@ class AuthManagerFlowManager(
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
result: AuthFlowResult,
) -> AuthFlowResult:
"""Return a user as result of login flow.
This method is called when a flow step returns FlowResultType.ABORT or
FlowResultType.CREATE_ENTRY.
"""
"""Return a user as result of login flow."""
flow = cast(LoginFlow, flow)
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_mail",
"google_maps",
"google_photos",
"google_pubsub",
"google_sheets",
"google_tasks",

View File

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

View File

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

View File

@@ -6,3 +6,52 @@ Component design guidelines:
format "<DOMAIN>.<OBJECT_ID>".
- 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 functools import partial
from pathlib import Path
from jaraco.abode.client import Client as Abode
import jaraco.abode.config
from jaraco.abode.exceptions import (
AuthenticationException as AbodeAuthenticationException,
Exception as AbodeException,
@@ -95,9 +93,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
password = entry.data[CONF_PASSWORD]
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
if entry.unique_id is None:
hass.config_entries.async_update_entry(

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
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 (
BinarySensorDeviceClass,

View File

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

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_push",
"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,
UnitOfIrradiance,
UnitOfLength,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
@@ -280,15 +279,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
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(
key="Precipitation",
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
@@ -298,16 +288,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
attr_fn=lambda data: {"type": data["PrecipitationType"]},
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(
key="PressureTendency",
device_class=SensorDeviceClass.ENUM,
@@ -315,19 +295,9 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
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(
key="UVIndex",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data),
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
@@ -354,7 +324,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription(
key="Wind",
device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
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.const import Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from .hub import PulseHub
@@ -18,9 +17,6 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool:
"""Set up Rollease Acmeda Automate hub from a config entry."""
await _migrate_unique_ids(hass, config_entry)
hub = PulseHub(hass, config_entry)
if not await hub.async_setup():
@@ -32,19 +28,6 @@ async def async_setup_entry(
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(
hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool:

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push",
"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 . import AcmedaConfigEntry
from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE
from .entity import AcmedaEntity
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."""
_attr_device_class = SensorDeviceClass.BATTERY

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
"""Support for Automation Device Specification (ADS)."""
import asyncio
from asyncio import timeout
from collections import namedtuple
import ctypes
import logging
import struct
import threading
import pyads
import voluptuous as vol
@@ -13,38 +19,42 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType
from .const import CONF_ADS_VAR, DATA_ADS, DOMAIN, AdsType
from .hub import AdsHub
_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 = {
AdsType.BOOL: pyads.PLCTYPE_BOOL,
AdsType.BYTE: pyads.PLCTYPE_BYTE,
AdsType.INT: pyads.PLCTYPE_INT,
AdsType.UINT: pyads.PLCTYPE_UINT,
AdsType.SINT: pyads.PLCTYPE_SINT,
AdsType.USINT: pyads.PLCTYPE_USINT,
AdsType.DINT: pyads.PLCTYPE_DINT,
AdsType.UDINT: pyads.PLCTYPE_UDINT,
AdsType.WORD: pyads.PLCTYPE_WORD,
AdsType.DWORD: pyads.PLCTYPE_DWORD,
AdsType.REAL: pyads.PLCTYPE_REAL,
AdsType.LREAL: pyads.PLCTYPE_LREAL,
AdsType.STRING: pyads.PLCTYPE_STRING,
AdsType.TIME: pyads.PLCTYPE_TIME,
AdsType.DATE: pyads.PLCTYPE_DATE,
AdsType.DATE_AND_TIME: pyads.PLCTYPE_DT,
AdsType.TOD: pyads.PLCTYPE_TOD,
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
ADSTYPE_DINT: pyads.PLCTYPE_DINT,
ADSTYPE_INT: pyads.PLCTYPE_INT,
ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
}
CONF_ADS_FACTOR = "factor"
CONF_ADS_TYPE = "adstype"
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"
@@ -63,7 +73,16 @@ CONFIG_SCHEMA = 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_VAR): cv.string,
}
@@ -97,9 +116,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
def handle_write_data_by_name(call: ServiceCall) -> None:
"""Write a value to the connected ADS device."""
ads_var: str = call.data[CONF_ADS_VAR]
ads_type: AdsType = call.data[CONF_ADS_TYPE]
value: int = call.data[CONF_ADS_VALUE]
ads_var = call.data[CONF_ADS_VAR]
ads_type = call.data[CONF_ADS_TYPE]
value = call.data[CONF_ADS_VALUE]
try:
ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type])
@@ -114,3 +133,181 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
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.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
from .entity import AdsEntity
from .hub import AdsHub
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
DEFAULT_NAME = "ADS binary sensor"
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
@@ -38,11 +36,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""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]
name: str = config[CONF_NAME]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
ads_var = config[CONF_ADS_VAR]
name = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS)
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
add_entities([ads_sensor])
@@ -51,13 +49,7 @@ def setup_platform(
class AdsBinarySensor(AdsEntity, BinarySensorEntity):
"""Representation of ADS binary sensors."""
def __init__(
self,
ads_hub: AdsHub,
name: str,
ads_var: str,
device_class: BinarySensorDeviceClass | None,
) -> None:
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var)
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,
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
@@ -21,9 +20,14 @@ 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, STATE_KEY_STATE
from .entity import AdsEntity
from .hub import AdsHub
from . import (
CONF_ADS_VAR,
CONF_ADS_VAR_POSITION,
DATA_ADS,
STATE_KEY_POSITION,
STATE_KEY_STATE,
AdsEntity,
)
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_CLOSE = "adsvar_close"
CONF_ADS_VAR_STOP = "adsvar_stop"
CONF_ADS_VAR_POSITION = "adsvar_position"
STATE_KEY_POSITION = "position"
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
{
@@ -58,14 +59,14 @@ def setup_platform(
"""Set up the cover platform for ADS."""
ads_hub = hass.data[DATA_ADS]
ads_var_is_closed: str = config[CONF_ADS_VAR]
ads_var_position: str | None = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set: str | None = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open: str | None = config.get(CONF_ADS_VAR_OPEN)
ads_var_close: str | None = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop: str | None = config.get(CONF_ADS_VAR_STOP)
name: str = config[CONF_NAME]
device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS)
ads_var_is_closed = config.get(CONF_ADS_VAR)
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
name = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS)
add_entities(
[
@@ -89,16 +90,16 @@ class AdsCover(AdsEntity, CoverEntity):
def __init__(
self,
ads_hub: AdsHub,
ads_var_is_closed: str,
ads_var_position: str | None,
ads_var_pos_set: str | None,
ads_var_open: str | None,
ads_var_close: str | None,
ads_var_stop: str | None,
name: str,
device_class: CoverDeviceClass | None,
) -> None:
ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class,
):
"""Initialize AdsCover entity."""
super().__init__(ads_hub, name, ads_var_is_closed)
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": {
"write_data_by_name": {
"service": "mdi:pencil"
}
"write_data_by_name": "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.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
from .entity import AdsEntity
from .hub import AdsHub
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
STATE_KEY_BRIGHTNESS = "brightness"
from . import (
CONF_ADS_VAR,
CONF_ADS_VAR_BRIGHTNESS,
DATA_ADS,
STATE_KEY_BRIGHTNESS,
STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
@@ -43,11 +45,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""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_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS)
name: str = config[CONF_NAME]
ads_var_enable = config[CONF_ADS_VAR]
ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS)
name = config[CONF_NAME]
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
@@ -55,13 +57,7 @@ def setup_platform(
class AdsLight(AdsEntity, LightEntity):
"""Representation of ADS light."""
def __init__(
self,
ads_hub: AdsHub,
ads_var_enable: str,
ads_var_brightness: str | None,
name: str,
) -> None:
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
"""Initialize AdsLight entity."""
super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None

View File

@@ -1,7 +1,7 @@
{
"domain": "ads",
"name": "ADS",
"codeowners": ["@mrpasztoradam"],
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ads",
"iot_class": "local_push",
"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
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
SensorDeviceClass,
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
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from . import ADS_TYPEMAP, CONF_ADS_FACTOR, CONF_ADS_TYPE
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsType
from .entity import AdsEntity
from .hub import AdsHub
from .. import ads
from . import (
ADS_TYPEMAP,
CONF_ADS_FACTOR,
CONF_ADS_TYPE,
CONF_ADS_VAR,
STATE_KEY_STATE,
AdsEntity,
)
DEFAULT_NAME = "ADS sensor"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
vol.Optional(CONF_ADS_TYPE, default=AdsType.INT): vol.All(
vol.Coerce(AdsType),
vol.In(
[
AdsType.BOOL,
AdsType.BYTE,
AdsType.INT,
AdsType.UINT,
AdsType.SINT,
AdsType.USINT,
AdsType.DINT,
AdsType.UDINT,
AdsType.WORD,
AdsType.DWORD,
AdsType.LREAL,
AdsType.REAL,
]
),
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In(
[
ads.ADSTYPE_INT,
ads.ADSTYPE_UINT,
ads.ADSTYPE_BYTE,
ads.ADSTYPE_DINT,
ads.ADSTYPE_UDINT,
]
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=""): cv.string,
}
)
@@ -64,26 +51,15 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""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_type: AdsType = config[CONF_ADS_TYPE]
name: str = config[CONF_NAME]
factor: int | None = config.get(CONF_ADS_FACTOR)
device_class: SensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
state_class: SensorStateClass | None = config.get(CONF_STATE_CLASS)
unit_of_measurement: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
ads_var = config[CONF_ADS_VAR]
ads_type = config[CONF_ADS_TYPE]
name = config[CONF_NAME]
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
factor = config.get(CONF_ADS_FACTOR)
entity = AdsSensor(
ads_hub,
ads_var,
ads_type,
name,
factor,
device_class,
state_class,
unit_of_measurement,
)
entity = AdsSensor(ads_hub, ads_var, ads_type, name, unit_of_measurement, factor)
add_entities([entity])
@@ -91,24 +67,12 @@ def setup_platform(
class AdsSensor(AdsEntity, SensorEntity):
"""Representation of an ADS sensor entity."""
def __init__(
self,
ads_hub: AdsHub,
ads_var: str,
ads_type: AdsType,
name: str,
factor: int | None,
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
unit_of_measurement: str | None,
) -> None:
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
"""Initialize AdsSensor entity."""
super().__init__(ads_hub, name, ads_var)
self._attr_native_unit_of_measurement = unit_of_measurement
self._ads_type = ads_type
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:
"""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.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
from .entity import AdsEntity
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
DEFAULT_NAME = "ADS Switch"
@@ -37,10 +36,10 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up switch platform for ADS."""
ads_hub = hass.data[DATA_ADS]
ads_hub = hass.data.get(DATA_ADS)
name: str = config[CONF_NAME]
ads_var: str = config[CONF_ADS_VAR]
name = config[CONF_NAME]
ads_var = config[CONF_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": {
"set_time_to": {
"service": "mdi:timer-cog"
}
"set_time_to": "mdi:timer-cog"
}
}

View File

@@ -6,7 +6,7 @@ from typing import Any
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 (
CONF_API_KEY,
CONF_LATITUDE,

View File

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

View File

@@ -1,19 +1,9 @@
{
"services": {
"start_recording": {
"service": "mdi:record-rec"
},
"stop_recording": {
"service": "mdi:stop"
},
"enable_alerts": {
"service": "mdi:bell-alert"
},
"disable_alerts": {
"service": "mdi:bell-off"
},
"snapshot": {
"service": "mdi:camera"
}
"start_recording": "mdi:record-rec",
"stop_recording": "mdi:stop",
"enable_alerts": "mdi:bell-alert",
"disable_alerts": "mdi:bell-off",
"snapshot": "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_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
_LOGGER: Final = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[AirQualityEntity]] = HassKey(DOMAIN)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
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:
"""Set up the air quality component."""
component = hass.data[DATA_COMPONENT] = EntityComponent[AirQualityEntity](
component = hass.data[DOMAIN] = EntityComponent[AirQualityEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
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:
"""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:
"""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):

View File

@@ -2,14 +2,18 @@
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.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
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] = [
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:
@@ -31,11 +43,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
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)

View File

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

View File

@@ -92,9 +92,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
except AirGradientError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(
current_measures.serial_number, raise_on_progress=False
)
await self.async_set_unique_id(current_measures.serial_number)
self._abort_if_unique_id_configured()
await self.set_configuration_source()
return self.async_create_entry(

View File

@@ -2,35 +2,24 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING
from airgradient import AirGradientClient, AirGradientError, Config, Measures
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
from .const import LOGGER
if TYPE_CHECKING:
from . import AirGradientConfigEntry
@dataclass
class AirGradientData:
"""Class for AirGradient data."""
measures: Measures
config: Config
class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Class to manage fetching AirGradient data."""
config_entry: AirGradientConfigEntry
_current_version: str
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
"""Initialize coordinator."""
@@ -44,27 +33,25 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
assert self.config_entry.unique_id
self.serial_number = self.config_entry.unique_id
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self._current_version = (
await self.client.get_current_measures()
).firmware_version
async def _async_update_data(self) -> AirGradientData:
async def _async_update_data(self) -> _DataT:
try:
measures = await self.client.get_current_measures()
config = await self.client.get_config()
return await self._update_data()
except AirGradientError as error:
raise UpdateFailed(error) from error
if measures.firmware_version != self._current_version:
device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, self.serial_number)}
)
assert device_entry
device_registry.async_update_device(
device_entry.id,
sw_version=measures.firmware_version,
)
self._current_version = measures.firmware_version
return AirGradientData(measures, config)
async def _update_data(self) -> _DataT:
raise NotImplementedError
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
"""Class to manage fetching AirGradient data."""
async def _update_data(self) -> Measures:
return await self.client.get_current_measures()
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
"""Class to manage fetching AirGradient data."""
async def _update_data(self) -> Config:
return await self.client.get_config()

View File

@@ -1,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."""
from airgradient import get_model_name
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -17,12 +15,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize airgradient entity."""
super().__init__(coordinator)
measures = coordinator.data.measures
self._attr_device_info = DeviceInfo(
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",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.9.0"],
"requirements": ["airgradient==0.8.0"],
"zeroconf": ["_airgradient._tcp.local."]
}

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType
from . import AirGradientConfigEntry
from .const import PM_STANDARD, PM_STANDARD_REVERSE
from .coordinator import AirGradientCoordinator
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
@@ -218,7 +218,7 @@ async def async_setup_entry(
) -> None:
"""Set up AirGradient sensor entities based on a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.measurement
listener: Callable[[], None] | None = None
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
MEASUREMENT_SENSOR_TYPES
@@ -232,7 +232,7 @@ async def async_setup_entry(
not_setup = set()
sensors = []
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)
else:
sensors.append(AirGradientMeasurementSensor(coordinator, description))
@@ -248,65 +248,64 @@ async def async_setup_entry(
add_entities()
entities = [
AirGradientConfigSensor(coordinator, description)
AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_SENSOR_TYPES
]
if "L" in coordinator.data.measures.model:
if "L" in coordinator.data.model:
entities.extend(
AirGradientConfigSensor(coordinator, description)
AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_LED_BAR_SENSOR_TYPES
)
if "I" in coordinator.data.measures.model:
if "I" in coordinator.data.model:
entities.extend(
AirGradientConfigSensor(coordinator, description)
AirGradientConfigSensor(entry.runtime_data.config, description)
for description in CONFIG_DISPLAY_SENSOR_TYPES
)
async_add_entities(entities)
class AirGradientSensor(AirGradientEntity, SensorEntity):
class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity):
"""Defines an AirGradient sensor."""
entity_description: AirGradientMeasurementSensorEntityDescription
coordinator: AirGradientMeasurementCoordinator
def __init__(
self,
coordinator: AirGradientCoordinator,
description: SensorEntityDescription,
coordinator: AirGradientMeasurementCoordinator,
description: AirGradientMeasurementSensorEntityDescription,
) -> None:
"""Initialize airgradient sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
class AirGradientMeasurementSensor(AirGradientSensor):
"""Defines an AirGradient sensor."""
entity_description: AirGradientMeasurementSensorEntityDescription
@property
def native_value(self) -> StateType:
"""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."""
entity_description: AirGradientConfigSensorEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__(
self,
coordinator: AirGradientCoordinator,
coordinator: AirGradientConfigCoordinator,
description: AirGradientConfigSensorEntityDescription,
) -> None:
"""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 = (
coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
)
@property
def native_value(self) -> StateType:
"""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 .const import DOMAIN
from .coordinator import AirGradientCoordinator
from .coordinator import AirGradientConfigCoordinator
from .entity import AirGradientEntity
@@ -46,7 +46,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AirGradient switch entities based on a config entry."""
coordinator = entry.runtime_data
coordinator = entry.runtime_data.config
added_entities = False
@@ -55,7 +55,7 @@ async def async_setup_entry(
nonlocal added_entities
if (
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
coordinator.data.configuration_control is ConfigurationControl.LOCAL
and not added_entities
):
async_add_entities(
@@ -63,8 +63,7 @@ async def async_setup_entry(
)
added_entities = True
elif (
coordinator.data.config.configuration_control
is not ConfigurationControl.LOCAL
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
and added_entities
):
entity_registry = er.async_get(hass)
@@ -83,10 +82,11 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
"""Defines an AirGradient switch entity."""
entity_description: AirGradientSwitchEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__(
self,
coordinator: AirGradientCoordinator,
coordinator: AirGradientConfigCoordinator,
description: AirGradientSwitchEntityDescription,
) -> None:
"""Initialize AirGradient switch."""
@@ -97,7 +97,7 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""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:
"""Turn the switch on."""

View File

@@ -7,7 +7,7 @@ from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry, AirGradientCoordinator
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
SCAN_INTERVAL = timedelta(hours=1)
@@ -20,17 +20,18 @@ async def async_setup_entry(
) -> None:
"""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):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
coordinator: AirGradientMeasurementCoordinator
def __init__(self, coordinator: AirGradientCoordinator) -> None:
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-update"
@@ -43,7 +44,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
@property
def installed_version(self) -> str:
"""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:
"""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.aiohttp_client import async_get_clientsession
from .const import DOMAIN # noqa: F401
from .coordinator import AirNowDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,32 +14,10 @@ ATTR_API_POLLUTANT = "Pollutant"
ATTR_API_REPORT_DATE = "DateObserved"
ATTR_API_REPORT_HOUR = "HourObserved"
ATTR_API_REPORT_TZ = "LocalTimeZone"
ATTR_API_REPORT_TZINFO = "LocalTimeZoneInfo"
ATTR_API_STATE = "StateCode"
ATTR_API_STATION = "ReportingArea"
ATTR_API_STATION_LATITUDE = "Latitude"
ATTR_API_STATION_LONGITUDE = "Longitude"
DEFAULT_NAME = "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.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
ATTR_API_AQI,
@@ -26,6 +27,7 @@ from .const import (
ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR,
ATTR_API_REPORT_TZ,
ATTR_API_REPORT_TZINFO,
ATTR_API_STATE,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
@@ -96,7 +98,9 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Copy Report Details
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
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
data[ATTR_API_STATE] = obv[ATTR_API_STATE]

View File

@@ -4,10 +4,9 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from dateutil import parser
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -35,13 +34,12 @@ from .const import (
ATTR_API_PM25,
ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR,
ATTR_API_REPORT_TZ,
ATTR_API_REPORT_TZINFO,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE,
DEFAULT_NAME,
DOMAIN,
US_TZ_OFFSETS,
)
ATTRIBUTION = "Data provided by AirNow"
@@ -71,18 +69,6 @@ def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
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, ...] = (
AirNowEntityDescription(
key=ATTR_API_AQI,
@@ -90,7 +76,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.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(
key=ATTR_API_PM10,

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"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")
return
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
await self._control(temp=temp)
class Airtouch5Zone(Airtouch5ClimateEntity):

View File

@@ -34,8 +34,13 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity import EntityDescription
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 (
CONF_CITY,
@@ -48,8 +53,6 @@ from .const import (
LOGGER,
)
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
# 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:
DOMAIN_AIRVISUAL_PRO = "airvisual_pro"
@@ -88,9 +91,10 @@ def async_get_cloud_coordinators_by_api_key(
) -> list[DataUpdateCoordinator]:
"""Get all DataUpdateCoordinator objects related to a particular API key."""
return [
entry.runtime_data
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.data.get(CONF_API_KEY) == api_key and hasattr(entry, "runtime_data")
coordinator
for entry_id, coordinator in hass.data[DOMAIN].items()
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)
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."""
if CONF_API_KEY not in entry.data:
# 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))
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
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
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."""
version = entry.version
@@ -383,18 +388,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry)
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok and CONF_API_KEY in entry.data:
# 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])
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if CONF_API_KEY in entry.data:
# 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
async def async_reload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> None:
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle an options update."""
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 homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
@@ -14,9 +15,9 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import AirVisualConfigEntry
from .const import CONF_CITY
from .const import CONF_CITY, DOMAIN
CONF_COORDINATES = "coordinates"
CONF_TITLE = "title"
@@ -36,10 +37,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirVisualConfigEntry
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return {
"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.update_coordinator import DataUpdateCoordinator
from . import AirVisualConfigEntry
from .const import CONF_CITY
from .entity import AirVisualEntity
from . import AirVisualEntity
from .const import CONF_CITY, DOMAIN
ATTR_CITY = "city"
ATTR_COUNTRY = "country"
@@ -106,12 +105,10 @@ POLLUTANT_UNITS = {
async def async_setup_entry(
hass: HomeAssistant,
entry: AirVisualConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up AirVisual sensors based on a config entry."""
coordinator = entry.runtime_data
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
AirVisualGeographySensor(coordinator, entry, description, locale)
for locale in GEOGRAPHY_SENSOR_LOCALES

View File

@@ -24,9 +24,15 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant
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]
@@ -114,3 +120,28 @@ async def async_unload_entry(
await entry.runtime_data.node.async_disconnect()
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."""
self._reauth_entry: ConfigEntry | None = None
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import a config entry from `airvisual` integration (see #83882)."""
return await self.async_step_user(import_data)
async def async_step_import(
self, import_config: dict[str, Any]
) -> ConfigFlowResult:
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
async def async_step_reauth(
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.helpers.entity_platform import AddEntitiesCallback
from . import AirVisualProConfigEntry
from .entity import AirVisualProEntity
from . import AirVisualProConfigEntry, AirVisualProEntity
@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 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.core import HomeAssistant

View File

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

View File

@@ -2,21 +2,16 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Final
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
from aioairzone.common import GrilleAngle, SleepTimeout
from aioairzone.const import (
API_COLD_ANGLE,
API_HEAT_ANGLE,
API_MODE,
API_SLEEP,
AZD_COLD_ANGLE,
AZD_HEAT_ANGLE,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_SLEEP,
AZD_ZONES,
)
@@ -38,9 +33,6 @@ class AirzoneSelectDescription(SelectEntityDescription):
api_param: str
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]] = {
@@ -50,15 +42,6 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
"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]] = {
"off": SleepTimeout.SLEEP_OFF,
"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, ...]] = (
AirzoneSelectDescription(
api_param=API_COLD_ANGLE,
@@ -132,20 +95,7 @@ async def async_setup_entry(
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities: list[AirzoneZoneSelect] = [
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in MAIN_ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
]
entities += [
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
@@ -156,8 +106,7 @@ async def async_setup_entry(
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
]
async_add_entities(entities)
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
@@ -204,11 +153,6 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
)
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._async_update_attrs()

View File

@@ -52,17 +52,6 @@
"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": {
"name": "Sleep",
"state": {

View File

@@ -21,7 +21,7 @@ from aioairzone_cloud.const import (
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.core import HomeAssistant

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"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_CPU_USAGE,
AZD_HUMIDITY,
AZD_INDOOR_EXCHANGER_TEMP,
AZD_INDOOR_RETURN_TEMP,
AZD_INDOOR_WORK_TEMP,
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_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_COVERAGE,
@@ -41,9 +32,7 @@ from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfElectricCurrent,
UnitOfInformation,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -59,78 +48,6 @@ from .entity import (
)
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(
device_class=SensorDeviceClass.TEMPERATURE,
key=AZD_TEMP,

View File

@@ -45,33 +45,6 @@
"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": {
"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_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401
_DEPRECATED_FORMAT_NUMBER,
@@ -53,7 +52,6 @@ from .const import ( # noqa: F401
_LOGGER: Final = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[AlarmControlPanelEntity]] = HassKey(DOMAIN)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
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:
"""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
)
@@ -124,12 +122,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""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:
"""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):

View File

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

View File

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

View File

@@ -2,8 +2,18 @@
from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
from typing import Any
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TITLE,
DOMAIN as DOMAIN_NOTIFY,
)
from homeassistant.const import (
CONF_ENTITY_ID,
CONF_NAME,
@@ -12,12 +22,22 @@ from homeassistant.const import (
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_IDLE,
STATE_OFF,
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
from homeassistant.helpers.entity import Entity
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.util.dt import now
from .const import (
CONF_ALERT_MESSAGE,
@@ -32,7 +52,6 @@ from .const import (
DOMAIN,
LOGGER,
)
from .entity import AlertEntity
ALERT_SCHEMA = vol.Schema(
{
@@ -64,9 +83,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""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():
if not cfg:
@@ -85,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
data = cfg.get(CONF_DATA)
entities.append(
AlertEntity(
Alert(
hass,
object_id,
name,
@@ -112,3 +131,183 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await component.async_add_entities(entities)
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": {
"toggle": {
"service": "mdi:bell-ring"
},
"turn_off": {
"service": "mdi:bell-off"
},
"turn_on": {
"service": "mdi:bell-alert"
}
"toggle": "mdi:bell-ring",
"turn_off": "mdi:bell-off",
"turn_on": "mdi:bell-alert"
}
}

View File

@@ -29,7 +29,6 @@ from homeassistant.components.alarm_control_panel import (
CodeFormat,
)
from homeassistant.components.climate import HVACMode
from homeassistant.components.lock import LockState
from homeassistant.const import (
ATTR_CODE_FORMAT,
ATTR_SUPPORTED_FEATURES,
@@ -41,12 +40,16 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_IDLE,
STATE_LOCKED,
STATE_LOCKING,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_UNLOCKED,
STATE_UNLOCKING,
UnitOfLength,
UnitOfMass,
UnitOfTemperature,
@@ -497,10 +500,10 @@ class AlexaLockController(AlexaCapability):
raise UnsupportedProperty(name)
# 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"
# 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 "JAMMED"

View File

@@ -661,12 +661,9 @@ class RemoteCapabilities(AlexaEntity):
def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)

View File

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

View File

@@ -7,13 +7,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
from .const import CONF_SITE_ID, PLATFORMS
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
from .coordinator import AmberUpdateCoordinator
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Amber Electric from a config entry."""
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
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)
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)
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."""
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,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AmberConfigEntry
from .const import ATTRIBUTION
from .const import ATTRIBUTION, DOMAIN
from .coordinator import AmberUpdateCoordinator
PRICE_SPIKE_ICONS = {
@@ -85,11 +85,11 @@ class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
async def async_setup_entry(
hass: HomeAssistant,
entry: AmberConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = entry.runtime_data
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
price_spike_description = BinarySensorEntityDescription(
key="price_spike",

View File

@@ -17,13 +17,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AmberConfigEntry
from .const import ATTRIBUTION
from .const import ATTRIBUTION, DOMAIN
from .coordinator import AmberUpdateCoordinator, normalize_descriptor
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
@@ -196,11 +196,11 @@ class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity):
async def async_setup_entry(
hass: HomeAssistant,
entry: AmberConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = entry.runtime_data
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
current: dict[str, CurrentInterval] = coordinator.data["current"]
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.core import HomeAssistant
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator
type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator]
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Ambient Weather Network from a config entry."""
api = OpenAPI()
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
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)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""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",
"iot_class": "cloud_polling",
"loggers": ["aioambient"],
"requirements": ["aioambient==2024.08.0"]
"requirements": ["aioambient==2024.01.0"]
}

View File

@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
@@ -28,7 +29,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from . import AmbientNetworkConfigEntry
from .const import DOMAIN
from .coordinator import AmbientNetworkDataUpdateCoordinator
from .entity import AmbientNetworkEntity
@@ -270,12 +271,12 @@ SENSOR_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: AmbientNetworkConfigEntry,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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:
async_add_entities(
AmbientNetworkSensor(

View File

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

View File

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

View File

@@ -261,19 +261,18 @@ class Analytics:
integrations.append(integration.domain)
if supervisor_info is not None:
supervisor_client = hassio.get_supervisor_client(hass)
installed_addons = await asyncio.gather(
*(
supervisor_client.addons.addon_info(addon[ATTR_SLUG])
hassio.async_get_addon_info(hass, addon[ATTR_SLUG])
for addon in supervisor_info[ATTR_ADDONS]
)
)
addons.extend(
{
ATTR_SLUG: addon.slug,
ATTR_PROTECTED: addon.protected,
ATTR_VERSION: addon.version,
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_SLUG: addon[ATTR_SLUG],
ATTR_PROTECTED: addon[ATTR_PROTECTED],
ATTR_VERSION: addon[ATTR_VERSION],
ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE],
}
for addon in installed_addons
)

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