mirror of
https://github.com/home-assistant/core.git
synced 2025-11-26 11:08:01 +00:00
Compare commits
1 Commits
light-stat
...
sql-query-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45aab80b59 |
@@ -14,7 +14,6 @@ core: &core
|
|||||||
base_platforms: &base_platforms
|
base_platforms: &base_platforms
|
||||||
- homeassistant/components/air_quality/**
|
- homeassistant/components/air_quality/**
|
||||||
- homeassistant/components/alarm_control_panel/**
|
- homeassistant/components/alarm_control_panel/**
|
||||||
- homeassistant/components/assist_satellite/**
|
|
||||||
- homeassistant/components/binary_sensor/**
|
- homeassistant/components/binary_sensor/**
|
||||||
- homeassistant/components/button/**
|
- homeassistant/components/button/**
|
||||||
- homeassistant/components/calendar/**
|
- homeassistant/components/calendar/**
|
||||||
@@ -62,7 +61,6 @@ components: &components
|
|||||||
- homeassistant/components/auth/**
|
- homeassistant/components/auth/**
|
||||||
- homeassistant/components/automation/**
|
- homeassistant/components/automation/**
|
||||||
- homeassistant/components/backup/**
|
- homeassistant/components/backup/**
|
||||||
- homeassistant/components/blueprint/**
|
|
||||||
- homeassistant/components/bluetooth/**
|
- homeassistant/components/bluetooth/**
|
||||||
- homeassistant/components/cloud/**
|
- homeassistant/components/cloud/**
|
||||||
- homeassistant/components/config/**
|
- homeassistant/components/config/**
|
||||||
@@ -111,7 +109,6 @@ components: &components
|
|||||||
- homeassistant/components/tag/**
|
- homeassistant/components/tag/**
|
||||||
- homeassistant/components/template/**
|
- homeassistant/components/template/**
|
||||||
- homeassistant/components/timer/**
|
- homeassistant/components/timer/**
|
||||||
- homeassistant/components/trace/**
|
|
||||||
- homeassistant/components/usb/**
|
- homeassistant/components/usb/**
|
||||||
- homeassistant/components/webhook/**
|
- homeassistant/components/webhook/**
|
||||||
- homeassistant/components/websocket_api/**
|
- homeassistant/components/websocket_api/**
|
||||||
@@ -127,11 +124,9 @@ tests: &tests
|
|||||||
- tests/*.py
|
- tests/*.py
|
||||||
- tests/auth/**
|
- tests/auth/**
|
||||||
- tests/backports/**
|
- tests/backports/**
|
||||||
- tests/components/diagnostics/**
|
|
||||||
- tests/components/history/**
|
- tests/components/history/**
|
||||||
- tests/components/logbook/**
|
- tests/components/logbook/**
|
||||||
- tests/components/recorder/**
|
- tests/components/recorder/**
|
||||||
- tests/components/repairs/**
|
|
||||||
- tests/components/sensor/**
|
- tests/components/sensor/**
|
||||||
- tests/hassfest/**
|
- tests/hassfest/**
|
||||||
- tests/helpers/**
|
- tests/helpers/**
|
||||||
|
|||||||
76
.github/workflows/builder.yml
vendored
76
.github/workflows/builder.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
UV_PRERELEASE: allow
|
UV_PRERELEASE: allow
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install "$(grep '^uv' < requirements.txt)"
|
python3 -m pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
uv pip install packaging tomli
|
uv pip install packaging tomli
|
||||||
uv pip install .
|
uv pip install .
|
||||||
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
||||||
@@ -242,7 +242,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -279,7 +279,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -316,12 +316,11 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.6.0
|
uses: sigstore/cosign-installer@v3.6.0
|
||||||
@@ -451,10 +450,10 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -483,56 +482,3 @@ jobs:
|
|||||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
|
|
||||||
twine upload dist/* --skip-existing
|
twine upload dist/* --skip-existing
|
||||||
|
|
||||||
hassfest-image:
|
|
||||||
name: Build and test hassfest image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
needs: ["init"]
|
|
||||||
if: github.repository_owner == 'home-assistant'
|
|
||||||
env:
|
|
||||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
|
||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
|
|
||||||
with:
|
|
||||||
context: . # So action will not pull the repository again
|
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
|
||||||
load: true
|
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
|
||||||
|
|
||||||
- name: Run hassfest against core
|
|
||||||
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
|
||||||
id: push
|
|
||||||
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
|
|
||||||
with:
|
|
||||||
context: . # So action will not pull the repository again
|
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
|
||||||
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
|
|||||||
141
.github/workflows/ci.yaml
vendored
141
.github/workflows/ci.yaml
vendored
@@ -39,8 +39,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
CACHE_VERSION: 10
|
CACHE_VERSION: 10
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 9
|
MYPY_CACHE_VERSION: 8
|
||||||
HA_SHORT_VERSION: "2024.11"
|
HA_SHORT_VERSION: "2024.9"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.12"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@@ -231,10 +231,10 @@ jobs:
|
|||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -252,7 +252,7 @@ jobs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pip install "$(grep '^uv' < requirements.txt)"
|
pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
@@ -277,9 +277,9 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@@ -317,9 +317,9 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@@ -357,9 +357,9 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@@ -429,32 +429,17 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
|
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
|
||||||
|
|
||||||
lint-hadolint:
|
|
||||||
name: Check ${{ matrix.file }}
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- info
|
|
||||||
if: |
|
|
||||||
github.event.inputs.pylint-only != 'true'
|
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
|
||||||
&& github.event.inputs.audit-licenses-only != 'true'
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
file:
|
|
||||||
- Dockerfile
|
|
||||||
- Dockerfile.dev
|
|
||||||
- script/hassfest/docker/Dockerfile
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.0
|
|
||||||
- name: Register hadolint problem matcher
|
- name: Register hadolint problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
- name: Check ${{ matrix.file }}
|
- name: Check Dockerfile
|
||||||
uses: docker://hadolint/hadolint:v2.12.0
|
uses: docker://hadolint/hadolint:v1.18.2
|
||||||
with:
|
with:
|
||||||
args: hadolint ${{ matrix.file }}
|
args: hadolint Dockerfile
|
||||||
|
- name: Check Dockerfile.dev
|
||||||
|
uses: docker://hadolint/hadolint:v1.18.2
|
||||||
|
with:
|
||||||
|
args: hadolint Dockerfile.dev
|
||||||
|
|
||||||
base:
|
base:
|
||||||
name: Prepare dependencies
|
name: Prepare dependencies
|
||||||
@@ -466,17 +451,17 @@ jobs:
|
|||||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Generate partial uv restore key
|
- name: Generate partial uv restore key
|
||||||
id: generate-uv-key
|
id: generate-uv-key
|
||||||
run: |
|
run: |
|
||||||
uv_version=$(cat requirements.txt | grep uv | cut -d '=' -f 3)
|
uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3)
|
||||||
echo "version=${uv_version}" >> $GITHUB_OUTPUT
|
echo "version=${uv_version}" >> $GITHUB_OUTPUT
|
||||||
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
|
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
|
||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
@@ -525,7 +510,7 @@ jobs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pip install "$(grep '^uv' < requirements.txt)"
|
pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
uv pip install -U "pip>=21.3.1" setuptools wheel
|
uv pip install -U "pip>=21.3.1" setuptools wheel
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
@@ -550,10 +535,10 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
libturbojpeg
|
libturbojpeg
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -583,10 +568,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -617,10 +602,10 @@ jobs:
|
|||||||
&& needs.info.outputs.requirements == 'true'
|
&& needs.info.outputs.requirements == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -638,14 +623,14 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip-licenses --format=json --output-file=licenses.json
|
pip-licenses --format=json --output-file=licenses.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: licenses
|
name: licenses
|
||||||
path: licenses.json
|
path: licenses.json
|
||||||
- name: Process licenses
|
- name: Process licenses
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses licenses.json
|
python -m script.licenses
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
@@ -660,10 +645,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -707,10 +692,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -752,10 +737,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -815,11 +800,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
strategy:
|
name: Split tests for full run
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
|
||||||
name: Split tests for full run Python ${{ matrix.python-version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -831,12 +812,12 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
@@ -852,7 +833,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
@@ -895,10 +876,10 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -953,14 +934,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@@ -1015,10 +996,10 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libmariadb-dev-compat
|
libmariadb-dev-compat
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -1079,7 +1060,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@@ -1087,7 +1068,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@@ -1141,10 +1122,10 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
postgresql-server-dev-14
|
postgresql-server-dev-14
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -1206,7 +1187,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@@ -1214,7 +1195,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@@ -1236,7 +1217,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
@@ -1287,10 +1268,10 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -1348,14 +1329,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@@ -1374,7 +1355,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.26.9
|
uses: github/codeql-action/init@v3.26.4
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.26.9
|
uses: github/codeql-action/analyze@v3.26.4
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
|||||||
56
.github/workflows/wheels.yml
vendored
56
.github/workflows/wheels.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pip install "$(grep '^uv' < requirements.txt)"
|
pip install "$(grep '^uv' < requirements_test.txt)"
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Get information
|
- name: Get information
|
||||||
@@ -82,15 +82,14 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
include-hidden-files: true
|
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@@ -102,7 +101,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@@ -119,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
@@ -131,12 +130,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Adjust build env
|
|
||||||
run: |
|
|
||||||
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
|
||||||
sed -i "/uv/d" requirements.txt
|
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
@@ -146,7 +139,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
|
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
|
||||||
skip-binary: aiohttp;multidict;yarl
|
skip-binary: aiohttp
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements.txt"
|
requirements: "requirements.txt"
|
||||||
@@ -163,7 +156,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.0
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
@@ -180,18 +173,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
- name: Adjust build env
|
|
||||||
run: |
|
|
||||||
if [ "${{ matrix.arch }}" = "i386" ]; then
|
|
||||||
echo "NPY_DISABLE_SVML=1" >> .env_file
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Do not pin numpy in wheels building
|
|
||||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
|
||||||
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
|
||||||
sed -i "/uv/d" requirements.txt
|
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
|
||||||
|
|
||||||
- name: Split requirements all
|
- name: Split requirements all
|
||||||
run: |
|
run: |
|
||||||
# We split requirements all into multiple files.
|
# We split requirements all into multiple files.
|
||||||
@@ -205,11 +186,22 @@ jobs:
|
|||||||
# Some dependencies still require 'cython<3'
|
# Some dependencies still require 'cython<3'
|
||||||
# and don't yet use isolated build environments.
|
# and don't yet use isolated build environments.
|
||||||
# Build these first.
|
# Build these first.
|
||||||
|
# grpcio: https://github.com/grpc/grpc/issues/33918
|
||||||
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
||||||
|
|
||||||
touch requirements_old-cython.txt
|
touch requirements_old-cython.txt
|
||||||
|
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt
|
||||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
||||||
|
|
||||||
|
- name: Adjust build env
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.arch }}" = "i386" ]; then
|
||||||
|
echo "NPY_DISABLE_SVML=1" >> .env_file
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Do not pin numpy in wheels building
|
||||||
|
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
- name: Build wheels (old cython)
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
@@ -219,7 +211,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_old-cython.txt"
|
requirements: "requirements_old-cython.txt"
|
||||||
@@ -234,7 +226,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtaa"
|
requirements: "requirements_all.txtaa"
|
||||||
@@ -248,7 +240,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtab"
|
requirements: "requirements_all.txtab"
|
||||||
@@ -262,7 +254,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtac"
|
requirements: "requirements_all.txtac"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.6.8
|
rev: v0.6.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@@ -83,7 +83,7 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
|
||||||
- id: hassfest-metadata
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ homeassistant.components.aruba.*
|
|||||||
homeassistant.components.arwn.*
|
homeassistant.components.arwn.*
|
||||||
homeassistant.components.aseko_pool_live.*
|
homeassistant.components.aseko_pool_live.*
|
||||||
homeassistant.components.assist_pipeline.*
|
homeassistant.components.assist_pipeline.*
|
||||||
homeassistant.components.assist_satellite.*
|
|
||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
homeassistant.components.autarco.*
|
homeassistant.components.autarco.*
|
||||||
homeassistant.components.auth.*
|
homeassistant.components.auth.*
|
||||||
@@ -111,7 +110,6 @@ homeassistant.components.bitcoin.*
|
|||||||
homeassistant.components.blockchain.*
|
homeassistant.components.blockchain.*
|
||||||
homeassistant.components.blue_current.*
|
homeassistant.components.blue_current.*
|
||||||
homeassistant.components.blueprint.*
|
homeassistant.components.blueprint.*
|
||||||
homeassistant.components.bluesound.*
|
|
||||||
homeassistant.components.bluetooth.*
|
homeassistant.components.bluetooth.*
|
||||||
homeassistant.components.bluetooth_adapters.*
|
homeassistant.components.bluetooth_adapters.*
|
||||||
homeassistant.components.bluetooth_tracker.*
|
homeassistant.components.bluetooth_tracker.*
|
||||||
@@ -141,7 +139,6 @@ homeassistant.components.cpuspeed.*
|
|||||||
homeassistant.components.crownstone.*
|
homeassistant.components.crownstone.*
|
||||||
homeassistant.components.date.*
|
homeassistant.components.date.*
|
||||||
homeassistant.components.datetime.*
|
homeassistant.components.datetime.*
|
||||||
homeassistant.components.deako.*
|
|
||||||
homeassistant.components.deconz.*
|
homeassistant.components.deconz.*
|
||||||
homeassistant.components.default_config.*
|
homeassistant.components.default_config.*
|
||||||
homeassistant.components.demo.*
|
homeassistant.components.demo.*
|
||||||
@@ -211,8 +208,6 @@ homeassistant.components.glances.*
|
|||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
homeassistant.components.google.*
|
homeassistant.components.google.*
|
||||||
homeassistant.components.google_assistant_sdk.*
|
homeassistant.components.google_assistant_sdk.*
|
||||||
homeassistant.components.google_cloud.*
|
|
||||||
homeassistant.components.google_photos.*
|
|
||||||
homeassistant.components.google_sheets.*
|
homeassistant.components.google_sheets.*
|
||||||
homeassistant.components.gpsd.*
|
homeassistant.components.gpsd.*
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
@@ -283,7 +278,6 @@ homeassistant.components.lawn_mower.*
|
|||||||
homeassistant.components.lcn.*
|
homeassistant.components.lcn.*
|
||||||
homeassistant.components.ld2410_ble.*
|
homeassistant.components.ld2410_ble.*
|
||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
|
||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
@@ -300,6 +294,7 @@ homeassistant.components.london_underground.*
|
|||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
|
homeassistant.components.mailbox.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.map.*
|
homeassistant.components.map.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
@@ -316,7 +311,6 @@ homeassistant.components.minecraft_server.*
|
|||||||
homeassistant.components.mjpeg.*
|
homeassistant.components.mjpeg.*
|
||||||
homeassistant.components.modbus.*
|
homeassistant.components.modbus.*
|
||||||
homeassistant.components.modem_callerid.*
|
homeassistant.components.modem_callerid.*
|
||||||
homeassistant.components.mold_indicator.*
|
|
||||||
homeassistant.components.monzo.*
|
homeassistant.components.monzo.*
|
||||||
homeassistant.components.moon.*
|
homeassistant.components.moon.*
|
||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
@@ -343,7 +337,6 @@ homeassistant.components.nut.*
|
|||||||
homeassistant.components.onboarding.*
|
homeassistant.components.onboarding.*
|
||||||
homeassistant.components.oncue.*
|
homeassistant.components.oncue.*
|
||||||
homeassistant.components.onewire.*
|
homeassistant.components.onewire.*
|
||||||
homeassistant.components.onkyo.*
|
|
||||||
homeassistant.components.open_meteo.*
|
homeassistant.components.open_meteo.*
|
||||||
homeassistant.components.openexchangerates.*
|
homeassistant.components.openexchangerates.*
|
||||||
homeassistant.components.opensky.*
|
homeassistant.components.opensky.*
|
||||||
@@ -403,7 +396,6 @@ homeassistant.components.select.*
|
|||||||
homeassistant.components.sensibo.*
|
homeassistant.components.sensibo.*
|
||||||
homeassistant.components.sensirion_ble.*
|
homeassistant.components.sensirion_ble.*
|
||||||
homeassistant.components.sensor.*
|
homeassistant.components.sensor.*
|
||||||
homeassistant.components.sensoterra.*
|
|
||||||
homeassistant.components.senz.*
|
homeassistant.components.senz.*
|
||||||
homeassistant.components.sfr_box.*
|
homeassistant.components.sfr_box.*
|
||||||
homeassistant.components.shelly.*
|
homeassistant.components.shelly.*
|
||||||
@@ -415,13 +407,10 @@ homeassistant.components.skybell.*
|
|||||||
homeassistant.components.slack.*
|
homeassistant.components.slack.*
|
||||||
homeassistant.components.sleepiq.*
|
homeassistant.components.sleepiq.*
|
||||||
homeassistant.components.smhi.*
|
homeassistant.components.smhi.*
|
||||||
homeassistant.components.smlight.*
|
|
||||||
homeassistant.components.snooz.*
|
homeassistant.components.snooz.*
|
||||||
homeassistant.components.solarlog.*
|
|
||||||
homeassistant.components.sonarr.*
|
homeassistant.components.sonarr.*
|
||||||
homeassistant.components.speedtestdotnet.*
|
homeassistant.components.speedtestdotnet.*
|
||||||
homeassistant.components.sql.*
|
homeassistant.components.sql.*
|
||||||
homeassistant.components.squeezebox.*
|
|
||||||
homeassistant.components.ssdp.*
|
homeassistant.components.ssdp.*
|
||||||
homeassistant.components.starlink.*
|
homeassistant.components.starlink.*
|
||||||
homeassistant.components.statistics.*
|
homeassistant.components.statistics.*
|
||||||
@@ -481,7 +470,6 @@ homeassistant.components.update.*
|
|||||||
homeassistant.components.uptime.*
|
homeassistant.components.uptime.*
|
||||||
homeassistant.components.uptimerobot.*
|
homeassistant.components.uptimerobot.*
|
||||||
homeassistant.components.usb.*
|
homeassistant.components.usb.*
|
||||||
homeassistant.components.uvc.*
|
|
||||||
homeassistant.components.vacuum.*
|
homeassistant.components.vacuum.*
|
||||||
homeassistant.components.vallox.*
|
homeassistant.components.vallox.*
|
||||||
homeassistant.components.valve.*
|
homeassistant.components.valve.*
|
||||||
|
|||||||
62
CODEOWNERS
62
CODEOWNERS
@@ -48,7 +48,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/adax/ @danielhiversen
|
/tests/components/adax/ @danielhiversen
|
||||||
/homeassistant/components/adguard/ @frenck
|
/homeassistant/components/adguard/ @frenck
|
||||||
/tests/components/adguard/ @frenck
|
/tests/components/adguard/ @frenck
|
||||||
/homeassistant/components/ads/ @mrpasztoradam
|
|
||||||
/homeassistant/components/advantage_air/ @Bre77
|
/homeassistant/components/advantage_air/ @Bre77
|
||||||
/tests/components/advantage_air/ @Bre77
|
/tests/components/advantage_air/ @Bre77
|
||||||
/homeassistant/components/aemet/ @Noltari
|
/homeassistant/components/aemet/ @Noltari
|
||||||
@@ -144,8 +143,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/aseko_pool_live/ @milanmeu
|
/tests/components/aseko_pool_live/ @milanmeu
|
||||||
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
||||||
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
||||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
|
||||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
|
||||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
||||||
/tests/components/asuswrt/ @kennedyshead @ollo69
|
/tests/components/asuswrt/ @kennedyshead @ollo69
|
||||||
/homeassistant/components/atag/ @MatsNL
|
/homeassistant/components/atag/ @MatsNL
|
||||||
@@ -231,16 +228,14 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/bsblan/ @liudger
|
/homeassistant/components/bsblan/ @liudger
|
||||||
/tests/components/bsblan/ @liudger
|
/tests/components/bsblan/ @liudger
|
||||||
/homeassistant/components/bt_smarthub/ @typhoon2099
|
/homeassistant/components/bt_smarthub/ @typhoon2099
|
||||||
/homeassistant/components/bthome/ @Ernst79 @thecode
|
/homeassistant/components/bthome/ @Ernst79
|
||||||
/tests/components/bthome/ @Ernst79 @thecode
|
/tests/components/bthome/ @Ernst79
|
||||||
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
|
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
|
||||||
/tests/components/buienradar/ @mjj4791 @ties @Robbie1221
|
/tests/components/buienradar/ @mjj4791 @ties @Robbie1221
|
||||||
/homeassistant/components/button/ @home-assistant/core
|
/homeassistant/components/button/ @home-assistant/core
|
||||||
/tests/components/button/ @home-assistant/core
|
/tests/components/button/ @home-assistant/core
|
||||||
/homeassistant/components/calendar/ @home-assistant/core
|
/homeassistant/components/calendar/ @home-assistant/core
|
||||||
/tests/components/calendar/ @home-assistant/core
|
/tests/components/calendar/ @home-assistant/core
|
||||||
/homeassistant/components/cambridge_audio/ @noahhusby
|
|
||||||
/tests/components/cambridge_audio/ @noahhusby
|
|
||||||
/homeassistant/components/camera/ @home-assistant/core
|
/homeassistant/components/camera/ @home-assistant/core
|
||||||
/tests/components/camera/ @home-assistant/core
|
/tests/components/camera/ @home-assistant/core
|
||||||
/homeassistant/components/cast/ @emontnemery
|
/homeassistant/components/cast/ @emontnemery
|
||||||
@@ -299,8 +294,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/date/ @home-assistant/core
|
/tests/components/date/ @home-assistant/core
|
||||||
/homeassistant/components/datetime/ @home-assistant/core
|
/homeassistant/components/datetime/ @home-assistant/core
|
||||||
/tests/components/datetime/ @home-assistant/core
|
/tests/components/datetime/ @home-assistant/core
|
||||||
/homeassistant/components/deako/ @sebirdman @balake @deakolights
|
|
||||||
/tests/components/deako/ @sebirdman @balake @deakolights
|
|
||||||
/homeassistant/components/debugpy/ @frenck
|
/homeassistant/components/debugpy/ @frenck
|
||||||
/tests/components/debugpy/ @frenck
|
/tests/components/debugpy/ @frenck
|
||||||
/homeassistant/components/deconz/ @Kane610
|
/homeassistant/components/deconz/ @Kane610
|
||||||
@@ -360,8 +353,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dsmr/ @Robbie1221
|
/tests/components/dsmr/ @Robbie1221
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/homeassistant/components/duke_energy/ @hunterjm
|
|
||||||
/tests/components/duke_energy/ @hunterjm
|
|
||||||
/homeassistant/components/duotecno/ @cereal2nd
|
/homeassistant/components/duotecno/ @cereal2nd
|
||||||
/tests/components/duotecno/ @cereal2nd
|
/tests/components/duotecno/ @cereal2nd
|
||||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||||
@@ -556,14 +547,11 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/google_assistant/ @home-assistant/cloud
|
/tests/components/google_assistant/ @home-assistant/cloud
|
||||||
/homeassistant/components/google_assistant_sdk/ @tronikos
|
/homeassistant/components/google_assistant_sdk/ @tronikos
|
||||||
/tests/components/google_assistant_sdk/ @tronikos
|
/tests/components/google_assistant_sdk/ @tronikos
|
||||||
/homeassistant/components/google_cloud/ @lufton @tronikos
|
/homeassistant/components/google_cloud/ @lufton
|
||||||
/tests/components/google_cloud/ @lufton @tronikos
|
|
||||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
||||||
/tests/components/google_generative_ai_conversation/ @tronikos
|
/tests/components/google_generative_ai_conversation/ @tronikos
|
||||||
/homeassistant/components/google_mail/ @tkdrob
|
/homeassistant/components/google_mail/ @tkdrob
|
||||||
/tests/components/google_mail/ @tkdrob
|
/tests/components/google_mail/ @tkdrob
|
||||||
/homeassistant/components/google_photos/ @allenporter
|
|
||||||
/tests/components/google_photos/ @allenporter
|
|
||||||
/homeassistant/components/google_sheets/ @tkdrob
|
/homeassistant/components/google_sheets/ @tkdrob
|
||||||
/tests/components/google_sheets/ @tkdrob
|
/tests/components/google_sheets/ @tkdrob
|
||||||
/homeassistant/components/google_tasks/ @allenporter
|
/homeassistant/components/google_tasks/ @allenporter
|
||||||
@@ -641,8 +629,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homewizard/ @DCSBL
|
/tests/components/homewizard/ @DCSBL
|
||||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||||
/tests/components/honeywell/ @rdfurman @mkmer
|
/tests/components/honeywell/ @rdfurman @mkmer
|
||||||
/homeassistant/components/html5/ @alexyao2015
|
|
||||||
/tests/components/html5/ @alexyao2015
|
|
||||||
/homeassistant/components/http/ @home-assistant/core
|
/homeassistant/components/http/ @home-assistant/core
|
||||||
/tests/components/http/ @home-assistant/core
|
/tests/components/http/ @home-assistant/core
|
||||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||||
@@ -721,8 +707,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ios/ @robbiet480
|
/tests/components/ios/ @robbiet480
|
||||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
/homeassistant/components/iotty/ @pburgio @shapournemati-iotty
|
/homeassistant/components/iotty/ @pburgio
|
||||||
/tests/components/iotty/ @pburgio @shapournemati-iotty
|
/tests/components/iotty/ @pburgio
|
||||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||||
/homeassistant/components/ipma/ @dgomes
|
/homeassistant/components/ipma/ @dgomes
|
||||||
/tests/components/ipma/ @dgomes
|
/tests/components/ipma/ @dgomes
|
||||||
@@ -735,8 +721,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/iron_os/ @tr4nt0r
|
/tests/components/iron_os/ @tr4nt0r
|
||||||
/homeassistant/components/isal/ @bdraco
|
/homeassistant/components/isal/ @bdraco
|
||||||
/tests/components/isal/ @bdraco
|
/tests/components/isal/ @bdraco
|
||||||
/homeassistant/components/iskra/ @iskramis
|
|
||||||
/tests/components/iskra/ @iskramis
|
|
||||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
/homeassistant/components/israel_rail/ @shaiu
|
/homeassistant/components/israel_rail/ @shaiu
|
||||||
@@ -813,8 +797,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/leaone/ @bdraco
|
/tests/components/leaone/ @bdraco
|
||||||
/homeassistant/components/led_ble/ @bdraco
|
/homeassistant/components/led_ble/ @bdraco
|
||||||
/tests/components/led_ble/ @bdraco
|
/tests/components/led_ble/ @bdraco
|
||||||
/homeassistant/components/lektrico/ @lektrico
|
|
||||||
/tests/components/lektrico/ @lektrico
|
|
||||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
@@ -861,8 +843,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lupusec/ @majuss @suaveolent
|
/tests/components/lupusec/ @majuss @suaveolent
|
||||||
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
||||||
/tests/components/lutron/ @cdheiser @wilburCForce
|
/tests/components/lutron/ @cdheiser @wilburCForce
|
||||||
/homeassistant/components/lutron_caseta/ @swails @danaues @eclair4151
|
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
|
||||||
/tests/components/lutron_caseta/ @swails @danaues @eclair4151
|
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
|
||||||
/homeassistant/components/lyric/ @timmo001
|
/homeassistant/components/lyric/ @timmo001
|
||||||
/tests/components/lyric/ @timmo001
|
/tests/components/lyric/ @timmo001
|
||||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||||
@@ -925,8 +907,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/modern_forms/ @wonderslug
|
/tests/components/modern_forms/ @wonderslug
|
||||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/homeassistant/components/monarch_money/ @jeeftor
|
|
||||||
/tests/components/monarch_money/ @jeeftor
|
|
||||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||||
/tests/components/monoprice/ @etsinko @OnFreund
|
/tests/components/monoprice/ @etsinko @OnFreund
|
||||||
/homeassistant/components/monzo/ @jakemartin-icl
|
/homeassistant/components/monzo/ @jakemartin-icl
|
||||||
@@ -1024,8 +1004,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nut/ @bdraco @ollo69 @pestevez
|
/tests/components/nut/ @bdraco @ollo69 @pestevez
|
||||||
/homeassistant/components/nws/ @MatthewFlamm @kamiyo
|
/homeassistant/components/nws/ @MatthewFlamm @kamiyo
|
||||||
/tests/components/nws/ @MatthewFlamm @kamiyo
|
/tests/components/nws/ @MatthewFlamm @kamiyo
|
||||||
/homeassistant/components/nyt_games/ @joostlek
|
|
||||||
/tests/components/nyt_games/ @joostlek
|
|
||||||
/homeassistant/components/nzbget/ @chriscla
|
/homeassistant/components/nzbget/ @chriscla
|
||||||
/tests/components/nzbget/ @chriscla
|
/tests/components/nzbget/ @chriscla
|
||||||
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
||||||
@@ -1104,6 +1082,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/pi_hole/ @shenxn
|
/tests/components/pi_hole/ @shenxn
|
||||||
/homeassistant/components/picnic/ @corneyl
|
/homeassistant/components/picnic/ @corneyl
|
||||||
/tests/components/picnic/ @corneyl
|
/tests/components/picnic/ @corneyl
|
||||||
|
/homeassistant/components/pilight/ @trekky12
|
||||||
|
/tests/components/pilight/ @trekky12
|
||||||
/homeassistant/components/ping/ @jpbede
|
/homeassistant/components/ping/ @jpbede
|
||||||
/tests/components/ping/ @jpbede
|
/tests/components/ping/ @jpbede
|
||||||
/homeassistant/components/plaato/ @JohNan
|
/homeassistant/components/plaato/ @JohNan
|
||||||
@@ -1133,8 +1113,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/proximity/ @mib1185
|
/homeassistant/components/proximity/ @mib1185
|
||||||
/tests/components/proximity/ @mib1185
|
/tests/components/proximity/ @mib1185
|
||||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||||
/homeassistant/components/prusalink/ @balloob
|
/homeassistant/components/prusalink/ @balloob @Skaronator
|
||||||
/tests/components/prusalink/ @balloob
|
/tests/components/prusalink/ @balloob @Skaronator
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||||
@@ -1295,8 +1275,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sensorpro/ @bdraco
|
/tests/components/sensorpro/ @bdraco
|
||||||
/homeassistant/components/sensorpush/ @bdraco
|
/homeassistant/components/sensorpush/ @bdraco
|
||||||
/tests/components/sensorpush/ @bdraco
|
/tests/components/sensorpush/ @bdraco
|
||||||
/homeassistant/components/sensoterra/ @markruys
|
|
||||||
/tests/components/sensoterra/ @markruys
|
|
||||||
/homeassistant/components/sentry/ @dcramer @frenck
|
/homeassistant/components/sentry/ @dcramer @frenck
|
||||||
/tests/components/sentry/ @dcramer @frenck
|
/tests/components/sentry/ @dcramer @frenck
|
||||||
/homeassistant/components/senz/ @milanmeu
|
/homeassistant/components/senz/ @milanmeu
|
||||||
@@ -1434,10 +1412,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/switchbee/ @jafar-atili
|
/tests/components/switchbee/ @jafar-atili
|
||||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
|
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||||
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
|
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||||
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
|
/homeassistant/components/switcher_kis/ @thecode
|
||||||
/tests/components/switcher_kis/ @thecode @YogevBokobza
|
/tests/components/switcher_kis/ @thecode
|
||||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||||
/homeassistant/components/syncthing/ @zhulik
|
/homeassistant/components/syncthing/ @zhulik
|
||||||
/tests/components/syncthing/ @zhulik
|
/tests/components/syncthing/ @zhulik
|
||||||
@@ -1515,8 +1493,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||||
/homeassistant/components/totalconnect/ @austinmroczek
|
/homeassistant/components/totalconnect/ @austinmroczek
|
||||||
/tests/components/totalconnect/ @austinmroczek
|
/tests/components/totalconnect/ @austinmroczek
|
||||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
|
||||||
/tests/components/touchline_sl/ @jnsgruk
|
|
||||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
|
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/homeassistant/components/tplink_omada/ @MarkGodwin
|
/homeassistant/components/tplink_omada/ @MarkGodwin
|
||||||
@@ -1541,8 +1517,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/transmission/ @engrbm87 @JPHutchins
|
/tests/components/transmission/ @engrbm87 @JPHutchins
|
||||||
/homeassistant/components/trend/ @jpbede
|
/homeassistant/components/trend/ @jpbede
|
||||||
/tests/components/trend/ @jpbede
|
/tests/components/trend/ @jpbede
|
||||||
/homeassistant/components/triggercmd/ @rvmey
|
|
||||||
/tests/components/triggercmd/ @rvmey
|
|
||||||
/homeassistant/components/tts/ @home-assistant/core
|
/homeassistant/components/tts/ @home-assistant/core
|
||||||
/tests/components/tts/ @home-assistant/core
|
/tests/components/tts/ @home-assistant/core
|
||||||
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
|
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
|
||||||
@@ -1647,8 +1621,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/webostv/ @thecode
|
/tests/components/webostv/ @thecode
|
||||||
/homeassistant/components/websocket_api/ @home-assistant/core
|
/homeassistant/components/websocket_api/ @home-assistant/core
|
||||||
/tests/components/websocket_api/ @home-assistant/core
|
/tests/components/websocket_api/ @home-assistant/core
|
||||||
/homeassistant/components/weheat/ @jesperraemaekers
|
|
||||||
/tests/components/weheat/ @jesperraemaekers
|
|
||||||
/homeassistant/components/wemo/ @esev
|
/homeassistant/components/wemo/ @esev
|
||||||
/tests/components/wemo/ @esev
|
/tests/components/wemo/ @esev
|
||||||
/homeassistant/components/whirlpool/ @abmantis @mkmer
|
/homeassistant/components/whirlpool/ @abmantis @mkmer
|
||||||
@@ -1666,8 +1638,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/wiz/ @sbidy
|
/tests/components/wiz/ @sbidy
|
||||||
/homeassistant/components/wled/ @frenck
|
/homeassistant/components/wled/ @frenck
|
||||||
/tests/components/wled/ @frenck
|
/tests/components/wled/ @frenck
|
||||||
/homeassistant/components/wmspro/ @mback2k
|
|
||||||
/tests/components/wmspro/ @mback2k
|
|
||||||
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
||||||
/tests/components/wolflink/ @adamkrol93 @mtielen
|
/tests/components/wolflink/ @adamkrol93 @mtielen
|
||||||
/homeassistant/components/workday/ @fabaff @gjohansson-ST
|
/homeassistant/components/workday/ @fabaff @gjohansson-ST
|
||||||
@@ -1688,8 +1658,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
|
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
|
||||||
/homeassistant/components/xiaomi_tv/ @simse
|
/homeassistant/components/xiaomi_tv/ @simse
|
||||||
/homeassistant/components/xmpp/ @fabaff @flowolf
|
/homeassistant/components/xmpp/ @fabaff @flowolf
|
||||||
/homeassistant/components/yale/ @bdraco
|
|
||||||
/tests/components/yale/ @bdraco
|
|
||||||
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
|
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
|
||||||
/tests/components/yale_smart_alarm/ @gjohansson-ST
|
/tests/components/yale_smart_alarm/ @gjohansson-ST
|
||||||
/homeassistant/components/yalexs_ble/ @bdraco
|
/homeassistant/components/yalexs_ble/ @bdraco
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -12,7 +12,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.4.15
|
RUN pip3 install uv==0.2.27
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
@@ -29,9 +29,15 @@ RUN \
|
|||||||
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
|
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
|
||||||
uv pip install homeassistant/home_assistant_*.whl; \
|
uv pip install homeassistant/home_assistant_*.whl; \
|
||||||
fi \
|
fi \
|
||||||
&& uv pip install \
|
&& if [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||||
--no-build \
|
linux32 uv pip install \
|
||||||
-r homeassistant/requirements_all.txt
|
--no-build \
|
||||||
|
-r homeassistant/requirements_all.txt; \
|
||||||
|
else \
|
||||||
|
uv pip install \
|
||||||
|
--no-build \
|
||||||
|
-r homeassistant/requirements_all.txt; \
|
||||||
|
fi
|
||||||
|
|
||||||
## Setup Home Assistant Core
|
## Setup Home Assistant Core
|
||||||
COPY . homeassistant/
|
COPY . homeassistant/
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ WORKDIR /usr/src
|
|||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||||
&& uv pip install --system -e hass-release/ \
|
&& uv pip install --system -e hass-release/
|
||||||
&& chown -R vscode /usr/src/hass-release/data
|
|
||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
|||||||
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||||
|
|
||||||
|
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|
||||||
|
|
||||||
|screenshot-states|
|
|screenshot-states|
|
||||||
|
|
||||||
Featured integrations
|
Featured integrations
|
||||||
@@ -20,14 +22,9 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
|
|||||||
If you run into issues while using Home Assistant or during development
|
If you run into issues while using Home Assistant or during development
|
||||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||||
|
|
||||||
|ohf-logo|
|
|
||||||
|
|
||||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||||
:target: https://www.home-assistant.io/join-chat/
|
:target: https://www.home-assistant.io/join-chat/
|
||||||
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
||||||
:target: https://demo.home-assistant.io
|
:target: https://demo.home-assistant.io
|
||||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
||||||
:target: https://home-assistant.io/integrations/
|
:target: https://home-assistant.io/integrations/
|
||||||
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
|
|
||||||
:alt: Home Assistant - A project from the Open Home Foundation
|
|
||||||
:target: https://www.openhomefoundation.org/
|
|
||||||
|
|||||||
@@ -127,11 +127,7 @@ class AuthManagerFlowManager(
|
|||||||
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
|
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
|
||||||
result: AuthFlowResult,
|
result: AuthFlowResult,
|
||||||
) -> AuthFlowResult:
|
) -> AuthFlowResult:
|
||||||
"""Return a user as result of login flow.
|
"""Return a user as result of login flow."""
|
||||||
|
|
||||||
This method is called when a flow step returns FlowResultType.ABORT or
|
|
||||||
FlowResultType.CREATE_ENTRY.
|
|
||||||
"""
|
|
||||||
flow = cast(LoginFlow, flow)
|
flow = cast(LoginFlow, flow)
|
||||||
|
|
||||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "aqara",
|
|
||||||
"name": "Aqara",
|
|
||||||
"iot_standards": ["matter", "zigbee"]
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
"google_generative_ai_conversation",
|
"google_generative_ai_conversation",
|
||||||
"google_mail",
|
"google_mail",
|
||||||
"google_maps",
|
"google_maps",
|
||||||
"google_photos",
|
|
||||||
"google_pubsub",
|
"google_pubsub",
|
||||||
"google_sheets",
|
"google_sheets",
|
||||||
"google_tasks",
|
"google_tasks",
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "roth",
|
|
||||||
"name": "Roth",
|
|
||||||
"integrations": ["touchline", "touchline_sl"]
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "yale",
|
"domain": "yale",
|
||||||
"name": "Yale",
|
"name": "Yale",
|
||||||
"integrations": [
|
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"]
|
||||||
"august",
|
|
||||||
"yale_smart_alarm",
|
|
||||||
"yalexs_ble",
|
|
||||||
"yale_home",
|
|
||||||
"yale"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,52 @@ Component design guidelines:
|
|||||||
format "<DOMAIN>.<OBJECT_ID>".
|
format "<DOMAIN>.<OBJECT_ID>".
|
||||||
- Each component should publish services only under its own domain.
|
- Each component should publish services only under its own domain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, split_entity_id
|
||||||
|
from homeassistant.helpers.frame import report
|
||||||
|
from homeassistant.helpers.group import expand_entity_ids
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
|
||||||
|
"""Load up the module to call the is_on method.
|
||||||
|
|
||||||
|
If there is no entity id given we will check all.
|
||||||
|
"""
|
||||||
|
report(
|
||||||
|
(
|
||||||
|
"uses homeassistant.components.is_on."
|
||||||
|
" This is deprecated and will stop working in Home Assistant 2024.9, it"
|
||||||
|
" should be updated to use the function of the platform directly."
|
||||||
|
),
|
||||||
|
error_if_core=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity_id:
|
||||||
|
entity_ids = expand_entity_ids(hass, [entity_id])
|
||||||
|
else:
|
||||||
|
entity_ids = hass.states.entity_ids()
|
||||||
|
|
||||||
|
for ent_id in entity_ids:
|
||||||
|
domain = split_entity_id(ent_id)[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
component = getattr(hass.components, domain)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not hasattr(component, "is_on"):
|
||||||
|
_LOGGER.warning("Integration %s has no is_on method", domain)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if component.is_on(ent_id):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from jaraco.abode.client import Client as Abode
|
from jaraco.abode.client import Client as Abode
|
||||||
import jaraco.abode.config
|
|
||||||
from jaraco.abode.exceptions import (
|
from jaraco.abode.exceptions import (
|
||||||
AuthenticationException as AbodeAuthenticationException,
|
AuthenticationException as AbodeAuthenticationException,
|
||||||
Exception as AbodeException,
|
Exception as AbodeException,
|
||||||
@@ -95,9 +93,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
password = entry.data[CONF_PASSWORD]
|
password = entry.data[CONF_PASSWORD]
|
||||||
polling = entry.data[CONF_POLLING]
|
polling = entry.data[CONF_POLLING]
|
||||||
|
|
||||||
# Configure abode library to use config directory for storing data
|
|
||||||
jaraco.abode.config.paths.override(user_data=Path(hass.config.path("Abode")))
|
|
||||||
|
|
||||||
# For previous config entries where unique_id is None
|
# For previous config entries where unique_id is None
|
||||||
if entry.unique_id is None:
|
if entry.unique_id is None:
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from jaraco.abode.devices.binary_sensor import BinarySensor
|
from jaraco.abode.devices.sensor import BinarySensor
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
|
|||||||
@@ -7,14 +7,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"capture_image": {
|
"capture_image": "mdi:camera",
|
||||||
"service": "mdi:camera"
|
"change_setting": "mdi:cog",
|
||||||
},
|
"trigger_automation": "mdi:play"
|
||||||
"change_setting": {
|
|
||||||
"service": "mdi:cog"
|
|
||||||
},
|
|
||||||
"trigger_automation": {
|
|
||||||
"service": "mdi:play"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["jaraco.abode", "lomond"],
|
"loggers": ["jaraco.abode", "lomond"],
|
||||||
"requirements": ["jaraco.abode==6.2.1"]
|
"requirements": ["jaraco.abode==5.2.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
|||||||
UV_INDEX,
|
UV_INDEX,
|
||||||
UnitOfIrradiance,
|
UnitOfIrradiance,
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfPressure,
|
|
||||||
UnitOfSpeed,
|
UnitOfSpeed,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
@@ -280,15 +279,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||||
translation_key="realfeel_temperature_shade",
|
translation_key="realfeel_temperature_shade",
|
||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
|
||||||
key="RelativeHumidity",
|
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
value_fn=lambda data: cast(int, data),
|
|
||||||
translation_key="humidity",
|
|
||||||
),
|
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="Precipitation",
|
key="Precipitation",
|
||||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||||
@@ -298,16 +288,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
attr_fn=lambda data: {"type": data["PrecipitationType"]},
|
attr_fn=lambda data: {"type": data["PrecipitationType"]},
|
||||||
translation_key="precipitation",
|
translation_key="precipitation",
|
||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
|
||||||
key="Pressure",
|
|
||||||
device_class=SensorDeviceClass.PRESSURE,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
suggested_display_precision=0,
|
|
||||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
|
||||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
|
||||||
translation_key="pressure",
|
|
||||||
),
|
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="PressureTendency",
|
key="PressureTendency",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
@@ -315,19 +295,9 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
||||||
translation_key="pressure_tendency",
|
translation_key="pressure_tendency",
|
||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
|
||||||
key="Temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
|
||||||
translation_key="temperature",
|
|
||||||
),
|
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="UVIndex",
|
key="UVIndex",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
native_unit_of_measurement=UV_INDEX,
|
native_unit_of_measurement=UV_INDEX,
|
||||||
value_fn=lambda data: cast(int, data),
|
value_fn=lambda data: cast(int, data),
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
|
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
|
||||||
@@ -354,7 +324,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="Wind",
|
key="Wind",
|
||||||
device_class=SensorDeviceClass.WIND_SPEED,
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
|
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.entity_registry as er
|
|
||||||
|
|
||||||
from .hub import PulseHub
|
from .hub import PulseHub
|
||||||
|
|
||||||
@@ -18,9 +17,6 @@ async def async_setup_entry(
|
|||||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up Rollease Acmeda Automate hub from a config entry."""
|
"""Set up Rollease Acmeda Automate hub from a config entry."""
|
||||||
|
|
||||||
await _migrate_unique_ids(hass, config_entry)
|
|
||||||
|
|
||||||
hub = PulseHub(hass, config_entry)
|
hub = PulseHub(hass, config_entry)
|
||||||
|
|
||||||
if not await hub.async_setup():
|
if not await hub.async_setup():
|
||||||
@@ -32,19 +28,6 @@ async def async_setup_entry(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _migrate_unique_ids(hass: HomeAssistant, entry: AcmedaConfigEntry) -> None:
|
|
||||||
"""Migrate pre-config flow unique ids."""
|
|
||||||
entity_registry = er.async_get(hass)
|
|
||||||
registry_entries = er.async_entries_for_config_entry(
|
|
||||||
entity_registry, entry.entry_id
|
|
||||||
)
|
|
||||||
for reg_entry in registry_entries:
|
|
||||||
if isinstance(reg_entry.unique_id, int): # type: ignore[unreachable]
|
|
||||||
entity_registry.async_update_entity( # type: ignore[unreachable]
|
|
||||||
reg_entry.entity_id, new_unique_id=str(reg_entry.unique_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(
|
||||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER
|
from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
class AcmedaEntity(entity.Entity):
|
class AcmedaBase(entity.Entity):
|
||||||
"""Base representation of an Acmeda roller."""
|
"""Base representation of an Acmeda roller."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
@@ -67,7 +67,7 @@ class AcmedaEntity(entity.Entity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return the unique ID of this roller."""
|
"""Return the unique ID of this roller."""
|
||||||
return str(self.roller.id)
|
return self.roller.id # type: ignore[no-any-return]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_id(self) -> str:
|
def device_id(self) -> str:
|
||||||
@@ -14,8 +14,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AcmedaConfigEntry
|
from . import AcmedaConfigEntry
|
||||||
|
from .base import AcmedaBase
|
||||||
from .const import ACMEDA_HUB_UPDATE
|
from .const import ACMEDA_HUB_UPDATE
|
||||||
from .entity import AcmedaEntity
|
|
||||||
from .helpers import async_add_acmeda_entities
|
from .helpers import async_add_acmeda_entities
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AcmedaCover(AcmedaEntity, CoverEntity):
|
class AcmedaCover(AcmedaBase, CoverEntity):
|
||||||
"""Representation of an Acmeda cover device."""
|
"""Representation of an Acmeda cover device."""
|
||||||
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiopulse"],
|
"loggers": ["aiopulse"],
|
||||||
"requirements": ["aiopulse==0.4.6"]
|
"requirements": ["aiopulse==0.4.4"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AcmedaConfigEntry
|
from . import AcmedaConfigEntry
|
||||||
|
from .base import AcmedaBase
|
||||||
from .const import ACMEDA_HUB_UPDATE
|
from .const import ACMEDA_HUB_UPDATE
|
||||||
from .entity import AcmedaEntity
|
|
||||||
from .helpers import async_add_acmeda_entities
|
from .helpers import async_add_acmeda_entities
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AcmedaBattery(AcmedaEntity, SensorEntity):
|
class AcmedaBattery(AcmedaBase, SensorEntity):
|
||||||
"""Representation of an Acmeda cover sensor."""
|
"""Representation of an Acmeda cover sensor."""
|
||||||
|
|
||||||
_attr_device_class = SensorDeviceClass.BATTERY
|
_attr_device_class = SensorDeviceClass.BATTERY
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Final
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
DOMAIN,
|
||||||
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
|
||||||
DeviceScanner,
|
DeviceScanner,
|
||||||
)
|
)
|
||||||
@@ -36,7 +36,7 @@ def get_scanner(
|
|||||||
hass: HomeAssistant, config: ConfigType
|
hass: HomeAssistant, config: ConfigType
|
||||||
) -> ActiontecDeviceScanner | None:
|
) -> ActiontecDeviceScanner | None:
|
||||||
"""Validate the configuration and return an Actiontec scanner."""
|
"""Validate the configuration and return an Actiontec scanner."""
|
||||||
scanner = ActiontecDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
|
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
self.last_results: list[Device] = []
|
self.last_results: list[Device] = []
|
||||||
data = self.get_actiontec_data()
|
data = self.get_actiontec_data()
|
||||||
self.success_init = data is not None
|
self.success_init = data is not None
|
||||||
|
_LOGGER.info("Scanner initialized")
|
||||||
|
|
||||||
def scan_devices(self) -> list[str]:
|
def scan_devices(self) -> list[str]:
|
||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
@@ -69,7 +70,7 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
Return boolean if scanning successful.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Scanning")
|
_LOGGER.info("Scanning")
|
||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
self.last_results = [
|
self.last_results = [
|
||||||
device for device in actiontec_data if device.timevalid > -60
|
device for device in actiontec_data if device.timevalid > -60
|
||||||
]
|
]
|
||||||
_LOGGER.debug("Scan successful")
|
_LOGGER.info("Scan successful")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_actiontec_data(self) -> list[Device] | None:
|
def get_actiontec_data(self) -> list[Device] | None:
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
async_get_clientsession(self.hass), account_id, password
|
async_get_clientsession(self.hass), account_id, password
|
||||||
)
|
)
|
||||||
if token is None:
|
if token is None:
|
||||||
_LOGGER.debug("Adax: Failed to login to retrieve token")
|
_LOGGER.info("Adax: Failed to login to retrieve token")
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="cloud",
|
step_id="cloud",
|
||||||
|
|||||||
@@ -66,20 +66,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_url": {
|
"add_url": "mdi:link-plus",
|
||||||
"service": "mdi:link-plus"
|
"remove_url": "mdi:link-off",
|
||||||
},
|
"enable_url": "mdi:link-variant",
|
||||||
"remove_url": {
|
"disable_url": "mdi:link-variant-off",
|
||||||
"service": "mdi:link-off"
|
"refresh": "mdi:refresh"
|
||||||
},
|
|
||||||
"enable_url": {
|
|
||||||
"service": "mdi:link-variant"
|
|
||||||
},
|
|
||||||
"disable_url": {
|
|
||||||
"service": "mdi:link-variant-off"
|
|
||||||
},
|
|
||||||
"refresh": {
|
|
||||||
"service": "mdi:refresh"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
"""Support for Automation Device Specification (ADS)."""
|
"""Support for Automation Device Specification (ADS)."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from asyncio import timeout
|
||||||
|
from collections import namedtuple
|
||||||
|
import ctypes
|
||||||
import logging
|
import logging
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
|
||||||
import pyads
|
import pyads
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -13,38 +19,42 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, DOMAIN, AdsType
|
|
||||||
from .hub import AdsHub
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_ADS = "data_ads"
|
||||||
|
|
||||||
|
# Supported Types
|
||||||
|
ADSTYPE_BOOL = "bool"
|
||||||
|
ADSTYPE_BYTE = "byte"
|
||||||
|
ADSTYPE_DINT = "dint"
|
||||||
|
ADSTYPE_INT = "int"
|
||||||
|
ADSTYPE_UDINT = "udint"
|
||||||
|
ADSTYPE_UINT = "uint"
|
||||||
|
|
||||||
ADS_TYPEMAP = {
|
ADS_TYPEMAP = {
|
||||||
AdsType.BOOL: pyads.PLCTYPE_BOOL,
|
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
|
||||||
AdsType.BYTE: pyads.PLCTYPE_BYTE,
|
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
|
||||||
AdsType.INT: pyads.PLCTYPE_INT,
|
ADSTYPE_DINT: pyads.PLCTYPE_DINT,
|
||||||
AdsType.UINT: pyads.PLCTYPE_UINT,
|
ADSTYPE_INT: pyads.PLCTYPE_INT,
|
||||||
AdsType.SINT: pyads.PLCTYPE_SINT,
|
ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
|
||||||
AdsType.USINT: pyads.PLCTYPE_USINT,
|
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
|
||||||
AdsType.DINT: pyads.PLCTYPE_DINT,
|
|
||||||
AdsType.UDINT: pyads.PLCTYPE_UDINT,
|
|
||||||
AdsType.WORD: pyads.PLCTYPE_WORD,
|
|
||||||
AdsType.DWORD: pyads.PLCTYPE_DWORD,
|
|
||||||
AdsType.REAL: pyads.PLCTYPE_REAL,
|
|
||||||
AdsType.LREAL: pyads.PLCTYPE_LREAL,
|
|
||||||
AdsType.STRING: pyads.PLCTYPE_STRING,
|
|
||||||
AdsType.TIME: pyads.PLCTYPE_TIME,
|
|
||||||
AdsType.DATE: pyads.PLCTYPE_DATE,
|
|
||||||
AdsType.DATE_AND_TIME: pyads.PLCTYPE_DT,
|
|
||||||
AdsType.TOD: pyads.PLCTYPE_TOD,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CONF_ADS_FACTOR = "factor"
|
CONF_ADS_FACTOR = "factor"
|
||||||
CONF_ADS_TYPE = "adstype"
|
CONF_ADS_TYPE = "adstype"
|
||||||
CONF_ADS_VALUE = "value"
|
CONF_ADS_VALUE = "value"
|
||||||
|
CONF_ADS_VAR = "adsvar"
|
||||||
|
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
|
||||||
|
CONF_ADS_VAR_POSITION = "adsvar_position"
|
||||||
|
|
||||||
|
STATE_KEY_STATE = "state"
|
||||||
|
STATE_KEY_BRIGHTNESS = "brightness"
|
||||||
|
STATE_KEY_POSITION = "position"
|
||||||
|
|
||||||
|
DOMAIN = "ads"
|
||||||
|
|
||||||
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
|
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
|
||||||
|
|
||||||
@@ -63,7 +73,16 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
|
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_TYPE): vol.Coerce(AdsType),
|
vol.Required(CONF_ADS_TYPE): vol.In(
|
||||||
|
[
|
||||||
|
ADSTYPE_INT,
|
||||||
|
ADSTYPE_UINT,
|
||||||
|
ADSTYPE_BYTE,
|
||||||
|
ADSTYPE_BOOL,
|
||||||
|
ADSTYPE_DINT,
|
||||||
|
ADSTYPE_UDINT,
|
||||||
|
]
|
||||||
|
),
|
||||||
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
|
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
}
|
}
|
||||||
@@ -97,9 +116,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
def handle_write_data_by_name(call: ServiceCall) -> None:
|
def handle_write_data_by_name(call: ServiceCall) -> None:
|
||||||
"""Write a value to the connected ADS device."""
|
"""Write a value to the connected ADS device."""
|
||||||
ads_var: str = call.data[CONF_ADS_VAR]
|
ads_var = call.data[CONF_ADS_VAR]
|
||||||
ads_type: AdsType = call.data[CONF_ADS_TYPE]
|
ads_type = call.data[CONF_ADS_TYPE]
|
||||||
value: int = call.data[CONF_ADS_VALUE]
|
value = call.data[CONF_ADS_VALUE]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type])
|
ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type])
|
||||||
@@ -114,3 +133,181 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Tuple to hold data needed for notification
|
||||||
|
NotificationItem = namedtuple( # noqa: PYI024
|
||||||
|
"NotificationItem", "hnotify huser name plc_datatype callback"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdsHub:
|
||||||
|
"""Representation of an ADS connection."""
|
||||||
|
|
||||||
|
def __init__(self, ads_client):
|
||||||
|
"""Initialize the ADS hub."""
|
||||||
|
self._client = ads_client
|
||||||
|
self._client.open()
|
||||||
|
|
||||||
|
# All ADS devices are registered here
|
||||||
|
self._devices = []
|
||||||
|
self._notification_items = {}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def shutdown(self, *args, **kwargs):
|
||||||
|
"""Shutdown ADS connection."""
|
||||||
|
|
||||||
|
_LOGGER.debug("Shutting down ADS")
|
||||||
|
for notification_item in self._notification_items.values():
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Deleting device notification %d, %d",
|
||||||
|
notification_item.hnotify,
|
||||||
|
notification_item.huser,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._client.del_device_notification(
|
||||||
|
notification_item.hnotify, notification_item.huser
|
||||||
|
)
|
||||||
|
except pyads.ADSError as err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
try:
|
||||||
|
self._client.close()
|
||||||
|
except pyads.ADSError as err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
|
||||||
|
def register_device(self, device):
|
||||||
|
"""Register a new device."""
|
||||||
|
self._devices.append(device)
|
||||||
|
|
||||||
|
def write_by_name(self, name, value, plc_datatype):
|
||||||
|
"""Write a value to the device."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
return self._client.write_by_name(name, value, plc_datatype)
|
||||||
|
except pyads.ADSError as err:
|
||||||
|
_LOGGER.error("Error writing %s: %s", name, err)
|
||||||
|
|
||||||
|
def read_by_name(self, name, plc_datatype):
|
||||||
|
"""Read a value from the device."""
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
return self._client.read_by_name(name, plc_datatype)
|
||||||
|
except pyads.ADSError as err:
|
||||||
|
_LOGGER.error("Error reading %s: %s", name, err)
|
||||||
|
|
||||||
|
def add_device_notification(self, name, plc_datatype, callback):
|
||||||
|
"""Add a notification to the ADS devices."""
|
||||||
|
|
||||||
|
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
hnotify, huser = self._client.add_device_notification(
|
||||||
|
name, attr, self._device_notification_callback
|
||||||
|
)
|
||||||
|
except pyads.ADSError as err:
|
||||||
|
_LOGGER.error("Error subscribing to %s: %s", name, err)
|
||||||
|
else:
|
||||||
|
hnotify = int(hnotify)
|
||||||
|
self._notification_items[hnotify] = NotificationItem(
|
||||||
|
hnotify, huser, name, plc_datatype, callback
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Added device notification %d for variable %s", hnotify, name
|
||||||
|
)
|
||||||
|
|
||||||
|
def _device_notification_callback(self, notification, name):
|
||||||
|
"""Handle device notifications."""
|
||||||
|
contents = notification.contents
|
||||||
|
|
||||||
|
hnotify = int(contents.hNotification)
|
||||||
|
_LOGGER.debug("Received notification %d", hnotify)
|
||||||
|
|
||||||
|
# get dynamically sized data array
|
||||||
|
data_size = contents.cbSampleSize
|
||||||
|
data = (ctypes.c_ubyte * data_size).from_address(
|
||||||
|
ctypes.addressof(contents)
|
||||||
|
+ pyads.structs.SAdsNotificationHeader.data.offset
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._lock:
|
||||||
|
notification_item = self._notification_items[hnotify]
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("Unknown device notification handle: %d", hnotify)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse data to desired datatype
|
||||||
|
if notification_item.plc_datatype == pyads.PLCTYPE_BOOL:
|
||||||
|
value = bool(struct.unpack("<?", bytearray(data))[0])
|
||||||
|
elif notification_item.plc_datatype == pyads.PLCTYPE_INT:
|
||||||
|
value = struct.unpack("<h", bytearray(data))[0]
|
||||||
|
elif notification_item.plc_datatype == pyads.PLCTYPE_BYTE:
|
||||||
|
value = struct.unpack("<B", bytearray(data))[0]
|
||||||
|
elif notification_item.plc_datatype == pyads.PLCTYPE_UINT:
|
||||||
|
value = struct.unpack("<H", bytearray(data))[0]
|
||||||
|
elif notification_item.plc_datatype == pyads.PLCTYPE_DINT:
|
||||||
|
value = struct.unpack("<i", bytearray(data))[0]
|
||||||
|
elif notification_item.plc_datatype == pyads.PLCTYPE_UDINT:
|
||||||
|
value = struct.unpack("<I", bytearray(data))[0]
|
||||||
|
else:
|
||||||
|
value = bytearray(data)
|
||||||
|
_LOGGER.warning("No callback available for this datatype")
|
||||||
|
|
||||||
|
notification_item.callback(notification_item.name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class AdsEntity(Entity):
|
||||||
|
"""Representation of ADS entity."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, ads_hub, name, ads_var):
|
||||||
|
"""Initialize ADS binary sensor."""
|
||||||
|
self._state_dict = {}
|
||||||
|
self._state_dict[STATE_KEY_STATE] = None
|
||||||
|
self._ads_hub = ads_hub
|
||||||
|
self._ads_var = ads_var
|
||||||
|
self._event = None
|
||||||
|
self._attr_unique_id = ads_var
|
||||||
|
self._attr_name = name
|
||||||
|
|
||||||
|
async def async_initialize_device(
|
||||||
|
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None
|
||||||
|
):
|
||||||
|
"""Register device notification."""
|
||||||
|
|
||||||
|
def update(name, value):
|
||||||
|
"""Handle device notifications."""
|
||||||
|
_LOGGER.debug("Variable %s changed its value to %d", name, value)
|
||||||
|
|
||||||
|
if factor is None:
|
||||||
|
self._state_dict[state_key] = value
|
||||||
|
else:
|
||||||
|
self._state_dict[state_key] = value / factor
|
||||||
|
|
||||||
|
asyncio.run_coroutine_threadsafe(async_event_set(), self.hass.loop)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
async def async_event_set():
|
||||||
|
"""Set event in async context."""
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
self._event = asyncio.Event()
|
||||||
|
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self._ads_hub.add_device_notification, ads_var, plctype, update
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
async with timeout(10):
|
||||||
|
await self._event.wait()
|
||||||
|
except TimeoutError:
|
||||||
|
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return False if state has not been updated yet."""
|
||||||
|
return self._state_dict[STATE_KEY_STATE] is not None
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
|
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
||||||
from .entity import AdsEntity
|
|
||||||
from .hub import AdsHub
|
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS binary sensor"
|
DEFAULT_NAME = "ADS binary sensor"
|
||||||
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
||||||
@@ -38,11 +36,11 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Binary Sensor platform for ADS."""
|
"""Set up the Binary Sensor platform for ADS."""
|
||||||
ads_hub = hass.data[DATA_ADS]
|
ads_hub = hass.data.get(DATA_ADS)
|
||||||
|
|
||||||
ads_var: str = config[CONF_ADS_VAR]
|
ads_var = config[CONF_ADS_VAR]
|
||||||
name: str = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
|
device_class = config.get(CONF_DEVICE_CLASS)
|
||||||
|
|
||||||
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
||||||
add_entities([ads_sensor])
|
add_entities([ads_sensor])
|
||||||
@@ -51,13 +49,7 @@ def setup_platform(
|
|||||||
class AdsBinarySensor(AdsEntity, BinarySensorEntity):
|
class AdsBinarySensor(AdsEntity, BinarySensorEntity):
|
||||||
"""Representation of ADS binary sensors."""
|
"""Representation of ADS binary sensors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||||
self,
|
|
||||||
ads_hub: AdsHub,
|
|
||||||
name: str,
|
|
||||||
ads_var: str,
|
|
||||||
device_class: BinarySensorDeviceClass | None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize ADS binary sensor."""
|
"""Initialize ADS binary sensor."""
|
||||||
super().__init__(ads_hub, name, ads_var)
|
super().__init__(ads_hub, name, ads_var)
|
||||||
self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING
|
self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -11,7 +11,6 @@ from homeassistant.components.cover import (
|
|||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA,
|
||||||
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
|
||||||
CoverDeviceClass,
|
|
||||||
CoverEntity,
|
CoverEntity,
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
@@ -21,9 +20,14 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
|
from . import (
|
||||||
from .entity import AdsEntity
|
CONF_ADS_VAR,
|
||||||
from .hub import AdsHub
|
CONF_ADS_VAR_POSITION,
|
||||||
|
DATA_ADS,
|
||||||
|
STATE_KEY_POSITION,
|
||||||
|
STATE_KEY_STATE,
|
||||||
|
AdsEntity,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS Cover"
|
DEFAULT_NAME = "ADS Cover"
|
||||||
|
|
||||||
@@ -31,9 +35,6 @@ CONF_ADS_VAR_SET_POS = "adsvar_set_position"
|
|||||||
CONF_ADS_VAR_OPEN = "adsvar_open"
|
CONF_ADS_VAR_OPEN = "adsvar_open"
|
||||||
CONF_ADS_VAR_CLOSE = "adsvar_close"
|
CONF_ADS_VAR_CLOSE = "adsvar_close"
|
||||||
CONF_ADS_VAR_STOP = "adsvar_stop"
|
CONF_ADS_VAR_STOP = "adsvar_stop"
|
||||||
CONF_ADS_VAR_POSITION = "adsvar_position"
|
|
||||||
|
|
||||||
STATE_KEY_POSITION = "position"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
@@ -58,14 +59,14 @@ def setup_platform(
|
|||||||
"""Set up the cover platform for ADS."""
|
"""Set up the cover platform for ADS."""
|
||||||
ads_hub = hass.data[DATA_ADS]
|
ads_hub = hass.data[DATA_ADS]
|
||||||
|
|
||||||
ads_var_is_closed: str = config[CONF_ADS_VAR]
|
ads_var_is_closed = config.get(CONF_ADS_VAR)
|
||||||
ads_var_position: str | None = config.get(CONF_ADS_VAR_POSITION)
|
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
|
||||||
ads_var_pos_set: str | None = config.get(CONF_ADS_VAR_SET_POS)
|
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
|
||||||
ads_var_open: str | None = config.get(CONF_ADS_VAR_OPEN)
|
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
|
||||||
ads_var_close: str | None = config.get(CONF_ADS_VAR_CLOSE)
|
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
|
||||||
ads_var_stop: str | None = config.get(CONF_ADS_VAR_STOP)
|
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
|
||||||
name: str = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS)
|
device_class = config.get(CONF_DEVICE_CLASS)
|
||||||
|
|
||||||
add_entities(
|
add_entities(
|
||||||
[
|
[
|
||||||
@@ -89,16 +90,16 @@ class AdsCover(AdsEntity, CoverEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ads_hub: AdsHub,
|
ads_hub,
|
||||||
ads_var_is_closed: str,
|
ads_var_is_closed,
|
||||||
ads_var_position: str | None,
|
ads_var_position,
|
||||||
ads_var_pos_set: str | None,
|
ads_var_pos_set,
|
||||||
ads_var_open: str | None,
|
ads_var_open,
|
||||||
ads_var_close: str | None,
|
ads_var_close,
|
||||||
ads_var_stop: str | None,
|
ads_var_stop,
|
||||||
name: str,
|
name,
|
||||||
device_class: CoverDeviceClass | None,
|
device_class,
|
||||||
) -> None:
|
):
|
||||||
"""Initialize AdsCover entity."""
|
"""Initialize AdsCover entity."""
|
||||||
super().__init__(ads_hub, name, ads_var_is_closed)
|
super().__init__(ads_hub, name, ads_var_is_closed)
|
||||||
if self._attr_unique_id is None:
|
if self._attr_unique_id is None:
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"write_data_by_name": {
|
"write_data_by_name": "mdi:pencil"
|
||||||
"service": "mdi:pencil"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
|
from . import (
|
||||||
from .entity import AdsEntity
|
CONF_ADS_VAR,
|
||||||
from .hub import AdsHub
|
CONF_ADS_VAR_BRIGHTNESS,
|
||||||
|
DATA_ADS,
|
||||||
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
|
STATE_KEY_BRIGHTNESS,
|
||||||
STATE_KEY_BRIGHTNESS = "brightness"
|
STATE_KEY_STATE,
|
||||||
|
AdsEntity,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS Light"
|
DEFAULT_NAME = "ADS Light"
|
||||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||||
@@ -43,11 +45,11 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the light platform for ADS."""
|
"""Set up the light platform for ADS."""
|
||||||
ads_hub = hass.data[DATA_ADS]
|
ads_hub = hass.data.get(DATA_ADS)
|
||||||
|
|
||||||
ads_var_enable: str = config[CONF_ADS_VAR]
|
ads_var_enable = config[CONF_ADS_VAR]
|
||||||
ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS)
|
ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS)
|
||||||
name: str = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
|
|
||||||
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
|
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
|
||||||
|
|
||||||
@@ -55,13 +57,7 @@ def setup_platform(
|
|||||||
class AdsLight(AdsEntity, LightEntity):
|
class AdsLight(AdsEntity, LightEntity):
|
||||||
"""Representation of ADS light."""
|
"""Representation of ADS light."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
|
||||||
self,
|
|
||||||
ads_hub: AdsHub,
|
|
||||||
ads_var_enable: str,
|
|
||||||
ads_var_brightness: str | None,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize AdsLight entity."""
|
"""Initialize AdsLight entity."""
|
||||||
super().__init__(ads_hub, name, ads_var_enable)
|
super().__init__(ads_hub, name, ads_var_enable)
|
||||||
self._state_dict[STATE_KEY_BRIGHTNESS] = None
|
self._state_dict[STATE_KEY_BRIGHTNESS] = None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "ads",
|
"domain": "ads",
|
||||||
"name": "ADS",
|
"name": "ADS",
|
||||||
"codeowners": ["@mrpasztoradam"],
|
"codeowners": [],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyads"],
|
"loggers": ["pyads"],
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -5,54 +5,41 @@ from __future__ import annotations
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
CONF_STATE_CLASS,
|
|
||||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
|
||||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||||
|
|
||||||
from . import ADS_TYPEMAP, CONF_ADS_FACTOR, CONF_ADS_TYPE
|
from .. import ads
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsType
|
from . import (
|
||||||
from .entity import AdsEntity
|
ADS_TYPEMAP,
|
||||||
from .hub import AdsHub
|
CONF_ADS_FACTOR,
|
||||||
|
CONF_ADS_TYPE,
|
||||||
|
CONF_ADS_VAR,
|
||||||
|
STATE_KEY_STATE,
|
||||||
|
AdsEntity,
|
||||||
|
)
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS sensor"
|
DEFAULT_NAME = "ADS sensor"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
|
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
|
||||||
vol.Optional(CONF_ADS_TYPE, default=AdsType.INT): vol.All(
|
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In(
|
||||||
vol.Coerce(AdsType),
|
[
|
||||||
vol.In(
|
ads.ADSTYPE_INT,
|
||||||
[
|
ads.ADSTYPE_UINT,
|
||||||
AdsType.BOOL,
|
ads.ADSTYPE_BYTE,
|
||||||
AdsType.BYTE,
|
ads.ADSTYPE_DINT,
|
||||||
AdsType.INT,
|
ads.ADSTYPE_UDINT,
|
||||||
AdsType.UINT,
|
]
|
||||||
AdsType.SINT,
|
|
||||||
AdsType.USINT,
|
|
||||||
AdsType.DINT,
|
|
||||||
AdsType.UDINT,
|
|
||||||
AdsType.WORD,
|
|
||||||
AdsType.DWORD,
|
|
||||||
AdsType.LREAL,
|
|
||||||
AdsType.REAL,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=""): cv.string,
|
||||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,26 +51,15 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up an ADS sensor device."""
|
"""Set up an ADS sensor device."""
|
||||||
ads_hub = hass.data[DATA_ADS]
|
ads_hub = hass.data.get(ads.DATA_ADS)
|
||||||
|
|
||||||
ads_var: str = config[CONF_ADS_VAR]
|
ads_var = config[CONF_ADS_VAR]
|
||||||
ads_type: AdsType = config[CONF_ADS_TYPE]
|
ads_type = config[CONF_ADS_TYPE]
|
||||||
name: str = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
factor: int | None = config.get(CONF_ADS_FACTOR)
|
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
device_class: SensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
|
factor = config.get(CONF_ADS_FACTOR)
|
||||||
state_class: SensorStateClass | None = config.get(CONF_STATE_CLASS)
|
|
||||||
unit_of_measurement: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
|
|
||||||
|
|
||||||
entity = AdsSensor(
|
entity = AdsSensor(ads_hub, ads_var, ads_type, name, unit_of_measurement, factor)
|
||||||
ads_hub,
|
|
||||||
ads_var,
|
|
||||||
ads_type,
|
|
||||||
name,
|
|
||||||
factor,
|
|
||||||
device_class,
|
|
||||||
state_class,
|
|
||||||
unit_of_measurement,
|
|
||||||
)
|
|
||||||
|
|
||||||
add_entities([entity])
|
add_entities([entity])
|
||||||
|
|
||||||
@@ -91,24 +67,12 @@ def setup_platform(
|
|||||||
class AdsSensor(AdsEntity, SensorEntity):
|
class AdsSensor(AdsEntity, SensorEntity):
|
||||||
"""Representation of an ADS sensor entity."""
|
"""Representation of an ADS sensor entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
|
||||||
self,
|
|
||||||
ads_hub: AdsHub,
|
|
||||||
ads_var: str,
|
|
||||||
ads_type: AdsType,
|
|
||||||
name: str,
|
|
||||||
factor: int | None,
|
|
||||||
device_class: SensorDeviceClass | None,
|
|
||||||
state_class: SensorStateClass | None,
|
|
||||||
unit_of_measurement: str | None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize AdsSensor entity."""
|
"""Initialize AdsSensor entity."""
|
||||||
super().__init__(ads_hub, name, ads_var)
|
super().__init__(ads_hub, name, ads_var)
|
||||||
|
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||||
self._ads_type = ads_type
|
self._ads_type = ads_type
|
||||||
self._factor = factor
|
self._factor = factor
|
||||||
self._attr_device_class = device_class
|
|
||||||
self._attr_state_class = state_class
|
|
||||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register device notification."""
|
"""Register device notification."""
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE
|
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
||||||
from .entity import AdsEntity
|
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS Switch"
|
DEFAULT_NAME = "ADS Switch"
|
||||||
|
|
||||||
@@ -37,10 +36,10 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up switch platform for ADS."""
|
"""Set up switch platform for ADS."""
|
||||||
ads_hub = hass.data[DATA_ADS]
|
ads_hub = hass.data.get(DATA_ADS)
|
||||||
|
|
||||||
name: str = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
ads_var: str = config[CONF_ADS_VAR]
|
ads_var = config[CONF_ADS_VAR]
|
||||||
|
|
||||||
add_entities([AdsSwitch(ads_hub, name, ads_var)])
|
add_entities([AdsSwitch(ads_hub, name, ads_var)])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"set_time_to": {
|
"set_time_to": "mdi:timer-cog"
|
||||||
"service": "mdi:timer-cog"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aemet_opendata.const import AOD_COORDS
|
from aemet_opendata.const import AOD_COORDS
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics.util import async_redact_data
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_tracking": {
|
"add_tracking": "mdi:package-variant-plus",
|
||||||
"service": "mdi:package-variant-plus"
|
"remove_tracking": "mdi:package-variant-minus"
|
||||||
},
|
|
||||||
"remove_tracking": {
|
|
||||||
"service": "mdi:package-variant-minus"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"start_recording": {
|
"start_recording": "mdi:record-rec",
|
||||||
"service": "mdi:record-rec"
|
"stop_recording": "mdi:stop",
|
||||||
},
|
"enable_alerts": "mdi:bell-alert",
|
||||||
"stop_recording": {
|
"disable_alerts": "mdi:bell-off",
|
||||||
"service": "mdi:stop"
|
"snapshot": "mdi:camera"
|
||||||
},
|
|
||||||
"enable_alerts": {
|
|
||||||
"service": "mdi:bell-alert"
|
|
||||||
},
|
|
||||||
"disable_alerts": {
|
|
||||||
"service": "mdi:bell-off"
|
|
||||||
},
|
|
||||||
"snapshot": {
|
|
||||||
"service": "mdi:camera"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,11 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.typing import ConfigType, StateType
|
from homeassistant.helpers.typing import ConfigType, StateType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_COMPONENT: HassKey[EntityComponent[AirQualityEntity]] = HassKey(DOMAIN)
|
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||||
@@ -56,7 +54,7 @@ PROP_TO_ATTR: Final[dict[str, str]] = {
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the air quality component."""
|
"""Set up the air quality component."""
|
||||||
component = hass.data[DATA_COMPONENT] = EntityComponent[AirQualityEntity](
|
component = hass.data[DOMAIN] = EntityComponent[AirQualityEntity](
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
@@ -65,12 +63,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN]
|
||||||
|
return await component.async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN]
|
||||||
|
return await component.async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class AirQualityEntity(Entity):
|
class AirQualityEntity(Entity):
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from airgradient import AirGradientClient
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, get_model_name
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .coordinator import AirGradientCoordinator
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
@@ -21,7 +25,15 @@ PLATFORMS: list[Platform] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
@dataclass
|
||||||
|
class AirGradientData:
|
||||||
|
"""AirGradient data class."""
|
||||||
|
|
||||||
|
measurement: AirGradientMeasurementCoordinator
|
||||||
|
config: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||||
@@ -31,11 +43,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
|||||||
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirGradientCoordinator(hass, client)
|
measurement_coordinator = AirGradientMeasurementCoordinator(hass, client)
|
||||||
|
config_coordinator = AirGradientConfigCoordinator(hass, client)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await measurement_coordinator.async_config_entry_first_refresh()
|
||||||
|
await config_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
device_registry = dr.async_get(hass)
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
||||||
|
manufacturer="AirGradient",
|
||||||
|
model=get_model_name(measurement_coordinator.data.model),
|
||||||
|
model_id=measurement_coordinator.data.model,
|
||||||
|
serial_number=measurement_coordinator.data.serial_number,
|
||||||
|
sw_version=measurement_coordinator.data.firmware_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.runtime_data = AirGradientData(
|
||||||
|
measurement=measurement_coordinator,
|
||||||
|
config=config_coordinator,
|
||||||
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import DOMAIN, AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
from .coordinator import AirGradientCoordinator
|
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -48,8 +47,8 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient button entities based on a config entry."""
|
"""Set up AirGradient button entities based on a config entry."""
|
||||||
coordinator = entry.runtime_data
|
model = entry.runtime_data.measurement.data.model
|
||||||
model = coordinator.data.measures.model
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
added_entities = False
|
added_entities = False
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ async def async_setup_entry(
|
|||||||
nonlocal added_entities
|
nonlocal added_entities
|
||||||
|
|
||||||
if (
|
if (
|
||||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
and not added_entities
|
and not added_entities
|
||||||
):
|
):
|
||||||
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
|
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
|
||||||
@@ -68,8 +67,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
added_entities = True
|
added_entities = True
|
||||||
elif (
|
elif (
|
||||||
coordinator.data.config.configuration_control
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
and added_entities
|
and added_entities
|
||||||
):
|
):
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
@@ -89,10 +87,11 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
|
|||||||
"""Defines an AirGradient button."""
|
"""Defines an AirGradient button."""
|
||||||
|
|
||||||
entity_description: AirGradientButtonEntityDescription
|
entity_description: AirGradientButtonEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientConfigCoordinator,
|
||||||
description: AirGradientButtonEntityDescription,
|
description: AirGradientButtonEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize airgradient button."""
|
"""Initialize airgradient button."""
|
||||||
|
|||||||
@@ -92,9 +92,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except AirGradientError:
|
except AirGradientError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(
|
await self.async_set_unique_id(current_measures.serial_number)
|
||||||
current_measures.serial_number, raise_on_progress=False
|
|
||||||
)
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
await self.set_configuration_source()
|
await self.set_configuration_source()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
|
|||||||
@@ -2,35 +2,24 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import LOGGER
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
class AirGradientData:
|
|
||||||
"""Class for AirGradient data."""
|
|
||||||
|
|
||||||
measures: Measures
|
|
||||||
config: Config
|
|
||||||
|
|
||||||
|
|
||||||
class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|
||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
config_entry: AirGradientConfigEntry
|
config_entry: AirGradientConfigEntry
|
||||||
_current_version: str
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
@@ -44,27 +33,25 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|||||||
assert self.config_entry.unique_id
|
assert self.config_entry.unique_id
|
||||||
self.serial_number = self.config_entry.unique_id
|
self.serial_number = self.config_entry.unique_id
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_update_data(self) -> _DataT:
|
||||||
"""Set up the coordinator."""
|
|
||||||
self._current_version = (
|
|
||||||
await self.client.get_current_measures()
|
|
||||||
).firmware_version
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> AirGradientData:
|
|
||||||
try:
|
try:
|
||||||
measures = await self.client.get_current_measures()
|
return await self._update_data()
|
||||||
config = await self.client.get_config()
|
|
||||||
except AirGradientError as error:
|
except AirGradientError as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
if measures.firmware_version != self._current_version:
|
|
||||||
device_registry = dr.async_get(self.hass)
|
async def _update_data(self) -> _DataT:
|
||||||
device_entry = device_registry.async_get_device(
|
raise NotImplementedError
|
||||||
identifiers={(DOMAIN, self.serial_number)}
|
|
||||||
)
|
|
||||||
assert device_entry
|
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||||
device_registry.async_update_device(
|
"""Class to manage fetching AirGradient data."""
|
||||||
device_entry.id,
|
|
||||||
sw_version=measures.firmware_version,
|
async def _update_data(self) -> Measures:
|
||||||
)
|
return await self.client.get_current_measures()
|
||||||
self._current_version = measures.firmware_version
|
|
||||||
return AirGradientData(measures, config)
|
|
||||||
|
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
||||||
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
|
async def _update_data(self) -> Config:
|
||||||
|
return await self.client.get_config()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"""Base class for AirGradient entities."""
|
"""Base class for AirGradient entities."""
|
||||||
|
|
||||||
from airgradient import get_model_name
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
@@ -17,12 +15,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
|||||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||||
"""Initialize airgradient entity."""
|
"""Initialize airgradient entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
measures = coordinator.data.measures
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, coordinator.serial_number)},
|
identifiers={(DOMAIN, coordinator.serial_number)},
|
||||||
manufacturer="AirGradient",
|
|
||||||
model=get_model_name(measures.model),
|
|
||||||
model_id=measures.model,
|
|
||||||
serial_number=coordinator.serial_number,
|
|
||||||
sw_version=measures.firmware_version,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airgradient==0.9.0"],
|
"requirements": ["airgradient==0.8.0"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient number entities based on a config entry."""
|
"""Set up AirGradient number entities based on a config entry."""
|
||||||
|
|
||||||
coordinator = entry.runtime_data
|
model = entry.runtime_data.measurement.data.model
|
||||||
model = coordinator.data.measures.model
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
added_entities = False
|
added_entities = False
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ async def async_setup_entry(
|
|||||||
nonlocal added_entities
|
nonlocal added_entities
|
||||||
|
|
||||||
if (
|
if (
|
||||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
and not added_entities
|
and not added_entities
|
||||||
):
|
):
|
||||||
entities = []
|
entities = []
|
||||||
@@ -84,8 +84,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
added_entities = True
|
added_entities = True
|
||||||
elif (
|
elif (
|
||||||
coordinator.data.config.configuration_control
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
and added_entities
|
and added_entities
|
||||||
):
|
):
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
@@ -105,10 +104,11 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
|
|||||||
"""Defines an AirGradient number entity."""
|
"""Defines an AirGradient number entity."""
|
||||||
|
|
||||||
entity_description: AirGradientNumberEntityDescription
|
entity_description: AirGradientNumberEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientConfigCoordinator,
|
||||||
description: AirGradientNumberEntityDescription,
|
description: AirGradientNumberEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize AirGradient number."""
|
"""Initialize AirGradient number."""
|
||||||
@@ -119,7 +119,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
|
|||||||
@property
|
@property
|
||||||
def native_value(self) -> int | None:
|
def native_value(self) -> int | None:
|
||||||
"""Return the state of the number."""
|
"""Return the state of the number."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Set the selected value."""
|
"""Set the selected value."""
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -144,11 +144,13 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient select entities based on a config entry."""
|
"""Set up AirGradient select entities based on a config entry."""
|
||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data.config
|
||||||
model = coordinator.data.measures.model
|
measurement_coordinator = entry.runtime_data.measurement
|
||||||
|
|
||||||
async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)])
|
async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)])
|
||||||
|
|
||||||
|
model = measurement_coordinator.data.model
|
||||||
|
|
||||||
added_entities = False
|
added_entities = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -156,7 +158,7 @@ async def async_setup_entry(
|
|||||||
nonlocal added_entities
|
nonlocal added_entities
|
||||||
|
|
||||||
if (
|
if (
|
||||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
and not added_entities
|
and not added_entities
|
||||||
):
|
):
|
||||||
entities: list[AirGradientSelect] = [
|
entities: list[AirGradientSelect] = [
|
||||||
@@ -177,8 +179,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
added_entities = True
|
added_entities = True
|
||||||
elif (
|
elif (
|
||||||
coordinator.data.config.configuration_control
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
and added_entities
|
and added_entities
|
||||||
):
|
):
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
@@ -200,10 +201,11 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
|||||||
"""Defines an AirGradient select entity."""
|
"""Defines an AirGradient select entity."""
|
||||||
|
|
||||||
entity_description: AirGradientSelectEntityDescription
|
entity_description: AirGradientSelectEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientConfigCoordinator,
|
||||||
description: AirGradientSelectEntityDescription,
|
description: AirGradientSelectEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize AirGradient select."""
|
"""Initialize AirGradient select."""
|
||||||
@@ -214,7 +216,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
|||||||
@property
|
@property
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
"""Return the state of the select."""
|
"""Return the state of the select."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType
|
|||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import PM_STANDARD, PM_STANDARD_REVERSE
|
from .const import PM_STANDARD, PM_STANDARD_REVERSE
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient sensor entities based on a config entry."""
|
"""Set up AirGradient sensor entities based on a config entry."""
|
||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data.measurement
|
||||||
listener: Callable[[], None] | None = None
|
listener: Callable[[], None] | None = None
|
||||||
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
|
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
|
||||||
MEASUREMENT_SENSOR_TYPES
|
MEASUREMENT_SENSOR_TYPES
|
||||||
@@ -232,7 +232,7 @@ async def async_setup_entry(
|
|||||||
not_setup = set()
|
not_setup = set()
|
||||||
sensors = []
|
sensors = []
|
||||||
for description in sensor_descriptions:
|
for description in sensor_descriptions:
|
||||||
if description.value_fn(coordinator.data.measures) is None:
|
if description.value_fn(coordinator.data) is None:
|
||||||
not_setup.add(description)
|
not_setup.add(description)
|
||||||
else:
|
else:
|
||||||
sensors.append(AirGradientMeasurementSensor(coordinator, description))
|
sensors.append(AirGradientMeasurementSensor(coordinator, description))
|
||||||
@@ -248,65 +248,64 @@ async def async_setup_entry(
|
|||||||
add_entities()
|
add_entities()
|
||||||
|
|
||||||
entities = [
|
entities = [
|
||||||
AirGradientConfigSensor(coordinator, description)
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
for description in CONFIG_SENSOR_TYPES
|
for description in CONFIG_SENSOR_TYPES
|
||||||
]
|
]
|
||||||
if "L" in coordinator.data.measures.model:
|
if "L" in coordinator.data.model:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
AirGradientConfigSensor(coordinator, description)
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
for description in CONFIG_LED_BAR_SENSOR_TYPES
|
for description in CONFIG_LED_BAR_SENSOR_TYPES
|
||||||
)
|
)
|
||||||
if "I" in coordinator.data.measures.model:
|
if "I" in coordinator.data.model:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
AirGradientConfigSensor(coordinator, description)
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
for description in CONFIG_DISPLAY_SENSOR_TYPES
|
for description in CONFIG_DISPLAY_SENSOR_TYPES
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AirGradientSensor(AirGradientEntity, SensorEntity):
|
class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity):
|
||||||
"""Defines an AirGradient sensor."""
|
"""Defines an AirGradient sensor."""
|
||||||
|
|
||||||
|
entity_description: AirGradientMeasurementSensorEntityDescription
|
||||||
|
coordinator: AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientMeasurementCoordinator,
|
||||||
description: SensorEntityDescription,
|
description: AirGradientMeasurementSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize airgradient sensor."""
|
"""Initialize airgradient sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
|
||||||
class AirGradientMeasurementSensor(AirGradientSensor):
|
|
||||||
"""Defines an AirGradient sensor."""
|
|
||||||
|
|
||||||
entity_description: AirGradientMeasurementSensorEntityDescription
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.measures)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
|
||||||
class AirGradientConfigSensor(AirGradientSensor):
|
class AirGradientConfigSensor(AirGradientEntity, SensorEntity):
|
||||||
"""Defines an AirGradient sensor."""
|
"""Defines an AirGradient sensor."""
|
||||||
|
|
||||||
entity_description: AirGradientConfigSensorEntityDescription
|
entity_description: AirGradientConfigSensorEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientConfigCoordinator,
|
||||||
description: AirGradientConfigSensorEntityDescription,
|
description: AirGradientConfigSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize airgradient sensor."""
|
"""Initialize airgradient sensor."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
self._attr_entity_registry_enabled_default = (
|
self._attr_entity_registry_enabled_default = (
|
||||||
coordinator.data.config.configuration_control
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirGradient switch entities based on a config entry."""
|
"""Set up AirGradient switch entities based on a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
added_entities = False
|
added_entities = False
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ async def async_setup_entry(
|
|||||||
nonlocal added_entities
|
nonlocal added_entities
|
||||||
|
|
||||||
if (
|
if (
|
||||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
and not added_entities
|
and not added_entities
|
||||||
):
|
):
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
@@ -63,8 +63,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
added_entities = True
|
added_entities = True
|
||||||
elif (
|
elif (
|
||||||
coordinator.data.config.configuration_control
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
and added_entities
|
and added_entities
|
||||||
):
|
):
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
@@ -83,10 +82,11 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
|||||||
"""Defines an AirGradient switch entity."""
|
"""Defines an AirGradient switch entity."""
|
||||||
|
|
||||||
entity_description: AirGradientSwitchEntityDescription
|
entity_description: AirGradientSwitchEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientCoordinator,
|
coordinator: AirGradientConfigCoordinator,
|
||||||
description: AirGradientSwitchEntityDescription,
|
description: AirGradientSwitchEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize AirGradient switch."""
|
"""Initialize AirGradient switch."""
|
||||||
@@ -97,7 +97,7 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return the state of the switch."""
|
"""Return the state of the switch."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AirGradientConfigEntry, AirGradientCoordinator
|
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(hours=1)
|
SCAN_INTERVAL = timedelta(hours=1)
|
||||||
@@ -20,17 +20,18 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Airgradient update platform."""
|
"""Set up Airgradient update platform."""
|
||||||
|
|
||||||
coordinator = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
|
|
||||||
async_add_entities([AirGradientUpdate(coordinator)], True)
|
async_add_entities([AirGradientUpdate(data.measurement)], True)
|
||||||
|
|
||||||
|
|
||||||
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||||
"""Representation of Airgradient Update."""
|
"""Representation of Airgradient Update."""
|
||||||
|
|
||||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||||
|
coordinator: AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}-update"
|
self._attr_unique_id = f"{coordinator.serial_number}-update"
|
||||||
@@ -43,7 +44,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
|||||||
@property
|
@property
|
||||||
def installed_version(self) -> str:
|
def installed_version(self) -> str:
|
||||||
"""Return the installed version of the entity."""
|
"""Return the installed version of the entity."""
|
||||||
return self.coordinator.data.measures.firmware_version
|
return self.coordinator.data.firmware_version
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update the entity."""
|
"""Update the entity."""
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN # noqa: F401
|
||||||
from .coordinator import AirNowDataUpdateCoordinator
|
from .coordinator import AirNowDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -14,32 +14,10 @@ ATTR_API_POLLUTANT = "Pollutant"
|
|||||||
ATTR_API_REPORT_DATE = "DateObserved"
|
ATTR_API_REPORT_DATE = "DateObserved"
|
||||||
ATTR_API_REPORT_HOUR = "HourObserved"
|
ATTR_API_REPORT_HOUR = "HourObserved"
|
||||||
ATTR_API_REPORT_TZ = "LocalTimeZone"
|
ATTR_API_REPORT_TZ = "LocalTimeZone"
|
||||||
|
ATTR_API_REPORT_TZINFO = "LocalTimeZoneInfo"
|
||||||
ATTR_API_STATE = "StateCode"
|
ATTR_API_STATE = "StateCode"
|
||||||
ATTR_API_STATION = "ReportingArea"
|
ATTR_API_STATION = "ReportingArea"
|
||||||
ATTR_API_STATION_LATITUDE = "Latitude"
|
ATTR_API_STATION_LATITUDE = "Latitude"
|
||||||
ATTR_API_STATION_LONGITUDE = "Longitude"
|
ATTR_API_STATION_LONGITUDE = "Longitude"
|
||||||
DEFAULT_NAME = "AirNow"
|
DEFAULT_NAME = "AirNow"
|
||||||
DOMAIN = "airnow"
|
DOMAIN = "airnow"
|
||||||
|
|
||||||
SECONDS_PER_HOUR = 3600
|
|
||||||
|
|
||||||
# AirNow seems to only use standard time zones,
|
|
||||||
# but we include daylight savings for completeness/futureproofing.
|
|
||||||
US_TZ_OFFSETS = {
|
|
||||||
"HST": -10 * SECONDS_PER_HOUR,
|
|
||||||
"HDT": -9 * SECONDS_PER_HOUR,
|
|
||||||
# AirNow returns AKT instead of AKST or AKDT, use standard
|
|
||||||
"AKT": -9 * SECONDS_PER_HOUR,
|
|
||||||
"AKST": -9 * SECONDS_PER_HOUR,
|
|
||||||
"AKDT": -8 * SECONDS_PER_HOUR,
|
|
||||||
"PST": -8 * SECONDS_PER_HOUR,
|
|
||||||
"PDT": -7 * SECONDS_PER_HOUR,
|
|
||||||
"MST": -7 * SECONDS_PER_HOUR,
|
|
||||||
"MDT": -6 * SECONDS_PER_HOUR,
|
|
||||||
"CST": -6 * SECONDS_PER_HOUR,
|
|
||||||
"CDT": -5 * SECONDS_PER_HOUR,
|
|
||||||
"EST": -5 * SECONDS_PER_HOUR,
|
|
||||||
"EDT": -4 * SECONDS_PER_HOUR,
|
|
||||||
"AST": -4 * SECONDS_PER_HOUR,
|
|
||||||
"ADT": -3 * SECONDS_PER_HOUR,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from pyairnow.errors import AirNowError
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_AQI,
|
ATTR_API_AQI,
|
||||||
@@ -26,6 +27,7 @@ from .const import (
|
|||||||
ATTR_API_REPORT_DATE,
|
ATTR_API_REPORT_DATE,
|
||||||
ATTR_API_REPORT_HOUR,
|
ATTR_API_REPORT_HOUR,
|
||||||
ATTR_API_REPORT_TZ,
|
ATTR_API_REPORT_TZ,
|
||||||
|
ATTR_API_REPORT_TZINFO,
|
||||||
ATTR_API_STATE,
|
ATTR_API_STATE,
|
||||||
ATTR_API_STATION,
|
ATTR_API_STATION,
|
||||||
ATTR_API_STATION_LATITUDE,
|
ATTR_API_STATION_LATITUDE,
|
||||||
@@ -96,7 +98,9 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
# Copy Report Details
|
# Copy Report Details
|
||||||
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
||||||
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
||||||
data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ]
|
data[ATTR_API_REPORT_TZINFO] = await dt_util.async_get_time_zone(
|
||||||
|
obv[ATTR_API_REPORT_TZ]
|
||||||
|
)
|
||||||
|
|
||||||
# Copy Station Details
|
# Copy Station Details
|
||||||
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dateutil import parser
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@@ -35,13 +34,12 @@ from .const import (
|
|||||||
ATTR_API_PM25,
|
ATTR_API_PM25,
|
||||||
ATTR_API_REPORT_DATE,
|
ATTR_API_REPORT_DATE,
|
||||||
ATTR_API_REPORT_HOUR,
|
ATTR_API_REPORT_HOUR,
|
||||||
ATTR_API_REPORT_TZ,
|
ATTR_API_REPORT_TZINFO,
|
||||||
ATTR_API_STATION,
|
ATTR_API_STATION,
|
||||||
ATTR_API_STATION_LATITUDE,
|
ATTR_API_STATION_LATITUDE,
|
||||||
ATTR_API_STATION_LONGITUDE,
|
ATTR_API_STATION_LONGITUDE,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
US_TZ_OFFSETS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by AirNow"
|
ATTRIBUTION = "Data provided by AirNow"
|
||||||
@@ -71,18 +69,6 @@ def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def aqi_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""Process extra attributes for main AQI sensor."""
|
|
||||||
return {
|
|
||||||
ATTR_DESCR: data[ATTR_API_AQI_DESCRIPTION],
|
|
||||||
ATTR_LEVEL: data[ATTR_API_AQI_LEVEL],
|
|
||||||
ATTR_TIME: parser.parse(
|
|
||||||
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}:00 {data[ATTR_API_REPORT_TZ]}",
|
|
||||||
tzinfos=US_TZ_OFFSETS,
|
|
||||||
).isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||||
AirNowEntityDescription(
|
AirNowEntityDescription(
|
||||||
key=ATTR_API_AQI,
|
key=ATTR_API_AQI,
|
||||||
@@ -90,7 +76,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
device_class=SensorDeviceClass.AQI,
|
device_class=SensorDeviceClass.AQI,
|
||||||
value_fn=lambda data: data.get(ATTR_API_AQI),
|
value_fn=lambda data: data.get(ATTR_API_AQI),
|
||||||
extra_state_attributes_fn=aqi_extra_attrs,
|
extra_state_attributes_fn=lambda data: {
|
||||||
|
ATTR_DESCR: data[ATTR_API_AQI_DESCRIPTION],
|
||||||
|
ATTR_LEVEL: data[ATTR_API_AQI_LEVEL],
|
||||||
|
ATTR_TIME: datetime.strptime(
|
||||||
|
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}",
|
||||||
|
"%Y-%m-%d %H",
|
||||||
|
)
|
||||||
|
.replace(tzinfo=data[ATTR_API_REPORT_TZINFO])
|
||||||
|
.isoformat(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
AirNowEntityDescription(
|
AirNowEntityDescription(
|
||||||
key=ATTR_API_PM10,
|
key=ATTR_API_PM10,
|
||||||
|
|||||||
@@ -24,5 +24,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==0.9.1"]
|
"requirements": ["airthings-ble==0.9.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
|
|||||||
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
|
await self._control(temp=temp)
|
||||||
|
|
||||||
|
|
||||||
class Airtouch5Zone(Airtouch5ClimateEntity):
|
class Airtouch5Zone(Airtouch5ClimateEntity):
|
||||||
|
|||||||
@@ -34,8 +34,13 @@ from homeassistant.helpers import (
|
|||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CITY,
|
CONF_CITY,
|
||||||
@@ -48,8 +53,6 @@ from .const import (
|
|||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
|
|
||||||
|
|
||||||
# We use a raw string for the airvisual_pro domain (instead of importing the actual
|
# We use a raw string for the airvisual_pro domain (instead of importing the actual
|
||||||
# constant) so that we can avoid listing it as a dependency:
|
# constant) so that we can avoid listing it as a dependency:
|
||||||
DOMAIN_AIRVISUAL_PRO = "airvisual_pro"
|
DOMAIN_AIRVISUAL_PRO = "airvisual_pro"
|
||||||
@@ -88,9 +91,10 @@ def async_get_cloud_coordinators_by_api_key(
|
|||||||
) -> list[DataUpdateCoordinator]:
|
) -> list[DataUpdateCoordinator]:
|
||||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||||
return [
|
return [
|
||||||
entry.runtime_data
|
coordinator
|
||||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
for entry_id, coordinator in hass.data[DOMAIN].items()
|
||||||
if entry.data.get(CONF_API_KEY) == api_key and hasattr(entry, "runtime_data")
|
if (entry := hass.config_entries.async_get_entry(entry_id))
|
||||||
|
and entry.data.get(CONF_API_KEY) == api_key
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -168,7 +172,7 @@ def _standardize_geography_config_entry(
|
|||||||
hass.config_entries.async_update_entry(entry, **entry_updates)
|
hass.config_entries.async_update_entry(entry, **entry_updates)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up AirVisual as config entry."""
|
"""Set up AirVisual as config entry."""
|
||||||
if CONF_API_KEY not in entry.data:
|
if CONF_API_KEY not in entry.data:
|
||||||
# If this is a migrated AirVisual Pro entry, there's no actual setup to do;
|
# If this is a migrated AirVisual Pro entry, there's no actual setup to do;
|
||||||
@@ -216,7 +220,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
|||||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = coordinator
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
# Reassess the interval between 2 server requests
|
# Reassess the interval between 2 server requests
|
||||||
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
||||||
@@ -226,7 +231,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Migrate an old config entry."""
|
"""Migrate an old config entry."""
|
||||||
version = entry.version
|
version = entry.version
|
||||||
|
|
||||||
@@ -383,18 +388,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry)
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload an AirVisual config entry."""
|
"""Unload an AirVisual config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok and CONF_API_KEY in entry.data:
|
if unload_ok:
|
||||||
# Re-calculate the update interval period for any remaining consumers of
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
# this API key:
|
if CONF_API_KEY in entry.data:
|
||||||
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
# Re-calculate the update interval period for any remaining consumers of
|
||||||
|
# this API key:
|
||||||
|
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
async def async_reload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> None:
|
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Handle an options update."""
|
"""Handle an options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualEntity(CoordinatorEntity):
|
||||||
|
"""Define a generic AirVisual entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: DataUpdateCoordinator,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self._attr_extra_state_attributes = {}
|
||||||
|
self._entry = entry
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Register callbacks."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update() -> None:
|
||||||
|
"""Update the state."""
|
||||||
|
self.update_from_latest_data()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
self.async_on_remove(self.coordinator.async_add_listener(update))
|
||||||
|
|
||||||
|
self.update_from_latest_data()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update_from_latest_data(self) -> None:
|
||||||
|
"""Update the entity from the latest data."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_COUNTRY,
|
CONF_COUNTRY,
|
||||||
@@ -14,9 +15,9 @@ from homeassistant.const import (
|
|||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from . import AirVisualConfigEntry
|
from .const import CONF_CITY, DOMAIN
|
||||||
from .const import CONF_CITY
|
|
||||||
|
|
||||||
CONF_COORDINATES = "coordinates"
|
CONF_COORDINATES = "coordinates"
|
||||||
CONF_TITLE = "title"
|
CONF_TITLE = "title"
|
||||||
@@ -36,10 +37,10 @@ TO_REDACT = {
|
|||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, entry: AirVisualConfigEntry
|
hass: HomeAssistant, entry: ConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -26,9 +26,8 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from . import AirVisualConfigEntry
|
from . import AirVisualEntity
|
||||||
from .const import CONF_CITY
|
from .const import CONF_CITY, DOMAIN
|
||||||
from .entity import AirVisualEntity
|
|
||||||
|
|
||||||
ATTR_CITY = "city"
|
ATTR_CITY = "city"
|
||||||
ATTR_COUNTRY = "country"
|
ATTR_COUNTRY = "country"
|
||||||
@@ -106,12 +105,10 @@ POLLUTANT_UNITS = {
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
entry: AirVisualConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AirVisual sensors based on a config entry."""
|
"""Set up AirVisual sensors based on a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AirVisualGeographySensor(coordinator, entry, description, locale)
|
AirVisualGeographySensor(coordinator, entry, description, locale)
|
||||||
for locale in GEOGRAPHY_SENSOR_LOCALES
|
for locale in GEOGRAPHY_SENSOR_LOCALES
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
@@ -114,3 +120,28 @@ async def async_unload_entry(
|
|||||||
await entry.runtime_data.node.async_disconnect()
|
await entry.runtime_data.node.async_disconnect()
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualProEntity(CoordinatorEntity):
|
||||||
|
"""Define a generic AirVisual Pro entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, coordinator: DataUpdateCoordinator, description: EntityDescription
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}"
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
|
||||||
|
manufacturer="AirVisual",
|
||||||
|
model=self.coordinator.data["status"]["model"],
|
||||||
|
name=self.coordinator.data["settings"]["node_name"],
|
||||||
|
hw_version=self.coordinator.data["status"]["system_version"],
|
||||||
|
sw_version=self.coordinator.data["status"]["app_version"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._reauth_entry: ConfigEntry | None = None
|
self._reauth_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
async def async_step_import(
|
||||||
"""Import a config entry from `airvisual` integration (see #83882)."""
|
self, import_config: dict[str, Any]
|
||||||
return await self.async_step_user(import_data)
|
) -> ConfigFlowResult:
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
return await self.async_step_user(import_config)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
|
|||||||
@@ -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"],
|
|
||||||
)
|
|
||||||
@@ -22,8 +22,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AirVisualProConfigEntry
|
from . import AirVisualProConfigEntry, AirVisualProEntity
|
||||||
from .entity import AirVisualProEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aioairzone.const import API_MAC, AZD_MAC
|
from aioairzone.const import API_MAC, AZD_MAC
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics.util import async_redact_data
|
||||||
from homeassistant.const import CONF_UNIQUE_ID
|
from homeassistant.const import CONF_UNIQUE_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.9.3"]
|
"requirements": ["aioairzone==0.8.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
|
from aioairzone.common import GrilleAngle, SleepTimeout
|
||||||
from aioairzone.const import (
|
from aioairzone.const import (
|
||||||
API_COLD_ANGLE,
|
API_COLD_ANGLE,
|
||||||
API_HEAT_ANGLE,
|
API_HEAT_ANGLE,
|
||||||
API_MODE,
|
|
||||||
API_SLEEP,
|
API_SLEEP,
|
||||||
AZD_COLD_ANGLE,
|
AZD_COLD_ANGLE,
|
||||||
AZD_HEAT_ANGLE,
|
AZD_HEAT_ANGLE,
|
||||||
AZD_MASTER,
|
|
||||||
AZD_MODE,
|
|
||||||
AZD_MODES,
|
|
||||||
AZD_SLEEP,
|
AZD_SLEEP,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
)
|
)
|
||||||
@@ -38,9 +33,6 @@ class AirzoneSelectDescription(SelectEntityDescription):
|
|||||||
|
|
||||||
api_param: str
|
api_param: str
|
||||||
options_dict: dict[str, int]
|
options_dict: dict[str, int]
|
||||||
options_fn: Callable[[dict[str, Any], dict[str, int]], list[str]] = (
|
|
||||||
lambda zone_data, value: list(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
||||||
@@ -50,15 +42,6 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
|||||||
"40deg": GrilleAngle.DEG_40,
|
"40deg": GrilleAngle.DEG_40,
|
||||||
}
|
}
|
||||||
|
|
||||||
MODE_DICT: Final[dict[str, int]] = {
|
|
||||||
"cool": OperationMode.COOLING,
|
|
||||||
"dry": OperationMode.DRY,
|
|
||||||
"fan": OperationMode.FAN,
|
|
||||||
"heat": OperationMode.HEATING,
|
|
||||||
"heat_cool": OperationMode.AUTO,
|
|
||||||
"stop": OperationMode.STOP,
|
|
||||||
}
|
|
||||||
|
|
||||||
SLEEP_DICT: Final[dict[str, int]] = {
|
SLEEP_DICT: Final[dict[str, int]] = {
|
||||||
"off": SleepTimeout.SLEEP_OFF,
|
"off": SleepTimeout.SLEEP_OFF,
|
||||||
"30m": SleepTimeout.SLEEP_30,
|
"30m": SleepTimeout.SLEEP_30,
|
||||||
@@ -67,26 +50,6 @@ SLEEP_DICT: Final[dict[str, int]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main_zone_options(
|
|
||||||
zone_data: dict[str, Any],
|
|
||||||
options: dict[str, int],
|
|
||||||
) -> list[str]:
|
|
||||||
"""Filter available modes."""
|
|
||||||
modes = zone_data.get(AZD_MODES, [])
|
|
||||||
return [k for k, v in options.items() if v in modes]
|
|
||||||
|
|
||||||
|
|
||||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
|
||||||
AirzoneSelectDescription(
|
|
||||||
api_param=API_MODE,
|
|
||||||
key=AZD_MODE,
|
|
||||||
options_dict=MODE_DICT,
|
|
||||||
options_fn=main_zone_options,
|
|
||||||
translation_key="modes",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||||
AirzoneSelectDescription(
|
AirzoneSelectDescription(
|
||||||
api_param=API_COLD_ANGLE,
|
api_param=API_COLD_ANGLE,
|
||||||
@@ -132,20 +95,7 @@ async def async_setup_entry(
|
|||||||
received_zones = set(zones_data)
|
received_zones = set(zones_data)
|
||||||
new_zones = received_zones - added_zones
|
new_zones = received_zones - added_zones
|
||||||
if new_zones:
|
if new_zones:
|
||||||
entities: list[AirzoneZoneSelect] = [
|
async_add_entities(
|
||||||
AirzoneZoneSelect(
|
|
||||||
coordinator,
|
|
||||||
description,
|
|
||||||
entry,
|
|
||||||
system_zone_id,
|
|
||||||
zones_data.get(system_zone_id),
|
|
||||||
)
|
|
||||||
for system_zone_id in new_zones
|
|
||||||
for description in MAIN_ZONE_SELECT_TYPES
|
|
||||||
if description.key in zones_data.get(system_zone_id)
|
|
||||||
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
|
|
||||||
]
|
|
||||||
entities += [
|
|
||||||
AirzoneZoneSelect(
|
AirzoneZoneSelect(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
@@ -156,8 +106,7 @@ async def async_setup_entry(
|
|||||||
for system_zone_id in new_zones
|
for system_zone_id in new_zones
|
||||||
for description in ZONE_SELECT_TYPES
|
for description in ZONE_SELECT_TYPES
|
||||||
if description.key in zones_data.get(system_zone_id)
|
if description.key in zones_data.get(system_zone_id)
|
||||||
]
|
)
|
||||||
async_add_entities(entities)
|
|
||||||
added_zones.update(new_zones)
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
@@ -204,11 +153,6 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
|||||||
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
self._attr_options = self.entity_description.options_fn(
|
|
||||||
zone_data, description.options_dict
|
|
||||||
)
|
|
||||||
|
|
||||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||||
|
|
||||||
self._async_update_attrs()
|
self._async_update_attrs()
|
||||||
|
|||||||
@@ -52,17 +52,6 @@
|
|||||||
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
|
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modes": {
|
|
||||||
"name": "Mode",
|
|
||||||
"state": {
|
|
||||||
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
|
|
||||||
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
|
|
||||||
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
|
|
||||||
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
|
|
||||||
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
|
|
||||||
"stop": "Stop"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sleep_times": {
|
"sleep_times": {
|
||||||
"name": "Sleep",
|
"name": "Sleep",
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from aioairzone_cloud.const import (
|
|||||||
RAW_WEBSERVERS,
|
RAW_WEBSERVERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics.util import async_redact_data
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.5"]
|
"requirements": ["aioairzone-cloud==0.6.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,7 @@ from aioairzone_cloud.const import (
|
|||||||
AZD_AQ_PM_10,
|
AZD_AQ_PM_10,
|
||||||
AZD_CPU_USAGE,
|
AZD_CPU_USAGE,
|
||||||
AZD_HUMIDITY,
|
AZD_HUMIDITY,
|
||||||
AZD_INDOOR_EXCHANGER_TEMP,
|
|
||||||
AZD_INDOOR_RETURN_TEMP,
|
|
||||||
AZD_INDOOR_WORK_TEMP,
|
|
||||||
AZD_MEMORY_FREE,
|
AZD_MEMORY_FREE,
|
||||||
AZD_OUTDOOR_CONDENSER_PRESS,
|
|
||||||
AZD_OUTDOOR_DISCHARGE_TEMP,
|
|
||||||
AZD_OUTDOOR_ELECTRIC_CURRENT,
|
|
||||||
AZD_OUTDOOR_EVAPORATOR_PRESS,
|
|
||||||
AZD_OUTDOOR_EXCHANGER_TEMP,
|
|
||||||
AZD_OUTDOOR_TEMP,
|
|
||||||
AZD_TEMP,
|
AZD_TEMP,
|
||||||
AZD_THERMOSTAT_BATTERY,
|
AZD_THERMOSTAT_BATTERY,
|
||||||
AZD_THERMOSTAT_COVERAGE,
|
AZD_THERMOSTAT_COVERAGE,
|
||||||
@@ -41,9 +32,7 @@ from homeassistant.const import (
|
|||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfElectricCurrent,
|
|
||||||
UnitOfInformation,
|
UnitOfInformation,
|
||||||
UnitOfPressure,
|
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@@ -59,78 +48,6 @@ from .entity import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_INDOOR_EXCHANGER_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="indoor_exchanger_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_INDOOR_RETURN_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="indoor_return_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_INDOOR_WORK_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="indoor_work_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_CONDENSER_PRESS,
|
|
||||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_condenser_press",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_DISCHARGE_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_discharge_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_ELECTRIC_CURRENT,
|
|
||||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_electric_current",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_EVAPORATOR_PRESS,
|
|
||||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_evaporator_press",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_EXCHANGER_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_exchanger_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
key=AZD_OUTDOOR_TEMP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
translation_key="outdoor_temp",
|
|
||||||
),
|
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
key=AZD_TEMP,
|
key=AZD_TEMP,
|
||||||
|
|||||||
@@ -45,33 +45,6 @@
|
|||||||
"free_memory": {
|
"free_memory": {
|
||||||
"name": "Free memory"
|
"name": "Free memory"
|
||||||
},
|
},
|
||||||
"indoor_exchanger_temp": {
|
|
||||||
"name": "Indoor exchanger temperature"
|
|
||||||
},
|
|
||||||
"indoor_return_temp": {
|
|
||||||
"name": "Indoor return temperature"
|
|
||||||
},
|
|
||||||
"indoor_work_temp": {
|
|
||||||
"name": "Indoor working temperature"
|
|
||||||
},
|
|
||||||
"outdoor_condenser_press": {
|
|
||||||
"name": "Outdoor condenser pressure"
|
|
||||||
},
|
|
||||||
"outdoor_discharge_temp": {
|
|
||||||
"name": "Outdoor discharge temperature"
|
|
||||||
},
|
|
||||||
"outdoor_electric_current": {
|
|
||||||
"name": "Outdoor electric current"
|
|
||||||
},
|
|
||||||
"outdoor_evaporator_press": {
|
|
||||||
"name": "Outdoor evaporator pressure"
|
|
||||||
},
|
|
||||||
"outdoor_exchanger_temp": {
|
|
||||||
"name": "Outdoor exchanger temperature"
|
|
||||||
},
|
|
||||||
"outdoor_temp": {
|
|
||||||
"name": "Outdoor temperature"
|
|
||||||
},
|
|
||||||
"thermostat_coverage": {
|
"thermostat_coverage": {
|
||||||
"name": "Signal percentage"
|
"name": "Signal percentage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ from homeassistant.helpers.deprecation import (
|
|||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
|
||||||
|
|
||||||
from .const import ( # noqa: F401
|
from .const import ( # noqa: F401
|
||||||
_DEPRECATED_FORMAT_NUMBER,
|
_DEPRECATED_FORMAT_NUMBER,
|
||||||
@@ -53,7 +52,6 @@ from .const import ( # noqa: F401
|
|||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_COMPONENT: HassKey[EntityComponent[AlarmControlPanelEntity]] = HassKey(DOMAIN)
|
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
||||||
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
||||||
@@ -71,7 +69,7 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Track states and offer events for sensors."""
|
"""Track states and offer events for sensors."""
|
||||||
component = hass.data[DATA_COMPONENT] = EntityComponent[AlarmControlPanelEntity](
|
component = hass.data[DOMAIN] = EntityComponent[AlarmControlPanelEntity](
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,12 +122,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN]
|
||||||
|
return await component.async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN]
|
||||||
|
return await component.async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||||
|
|||||||
@@ -15,26 +15,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_arm_away": {
|
"alarm_arm_away": "mdi:shield-lock",
|
||||||
"service": "mdi:shield-lock"
|
"alarm_arm_home": "mdi:shield-home",
|
||||||
},
|
"alarm_arm_night": "mdi:shield-moon",
|
||||||
"alarm_arm_home": {
|
"alarm_arm_custom_bypass": "mdi:security",
|
||||||
"service": "mdi:shield-home"
|
"alarm_disarm": "mdi:shield-off",
|
||||||
},
|
"alarm_trigger": "mdi:bell-ring",
|
||||||
"alarm_arm_night": {
|
"alarm_arm_vacation": "mdi:shield-airplane"
|
||||||
"service": "mdi:shield-moon"
|
|
||||||
},
|
|
||||||
"alarm_arm_custom_bypass": {
|
|
||||||
"service": "mdi:security"
|
|
||||||
},
|
|
||||||
"alarm_disarm": {
|
|
||||||
"service": "mdi:shield-off"
|
|
||||||
},
|
|
||||||
"alarm_trigger": {
|
|
||||||
"service": "mdi:bell-ring"
|
|
||||||
},
|
|
||||||
"alarm_arm_vacation": {
|
|
||||||
"service": "mdi:shield-airplane"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_keypress": {
|
"alarm_keypress": "mdi:dialpad",
|
||||||
"service": "mdi:dialpad"
|
"alarm_toggle_chime": "mdi:abc"
|
||||||
},
|
|
||||||
"alarm_toggle_chime": {
|
|
||||||
"service": "mdi:abc"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.notify import (
|
||||||
|
ATTR_DATA,
|
||||||
|
ATTR_MESSAGE,
|
||||||
|
ATTR_TITLE,
|
||||||
|
DOMAIN as DOMAIN_NOTIFY,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@@ -12,12 +22,22 @@ from homeassistant.const import (
|
|||||||
SERVICE_TOGGLE,
|
SERVICE_TOGGLE,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
|
STATE_IDLE,
|
||||||
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_point_in_time,
|
||||||
|
async_track_state_change_event,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.util.dt import now
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALERT_MESSAGE,
|
CONF_ALERT_MESSAGE,
|
||||||
@@ -32,7 +52,6 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
from .entity import AlertEntity
|
|
||||||
|
|
||||||
ALERT_SCHEMA = vol.Schema(
|
ALERT_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -64,9 +83,9 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Alert component."""
|
"""Set up the Alert component."""
|
||||||
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
component = EntityComponent[Alert](LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
entities: list[AlertEntity] = []
|
entities: list[Alert] = []
|
||||||
|
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
if not cfg:
|
if not cfg:
|
||||||
@@ -85,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
data = cfg.get(CONF_DATA)
|
data = cfg.get(CONF_DATA)
|
||||||
|
|
||||||
entities.append(
|
entities.append(
|
||||||
AlertEntity(
|
Alert(
|
||||||
hass,
|
hass,
|
||||||
object_id,
|
object_id,
|
||||||
name,
|
name,
|
||||||
@@ -112,3 +131,183 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
await component.async_add_entities(entities)
|
await component.async_add_entities(entities)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Alert(Entity):
|
||||||
|
"""Representation of an alert."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: str,
|
||||||
|
name: str,
|
||||||
|
watched_entity_id: str,
|
||||||
|
state: str,
|
||||||
|
repeat: list[float],
|
||||||
|
skip_first: bool,
|
||||||
|
message_template: Template | None,
|
||||||
|
done_message_template: Template | None,
|
||||||
|
notifiers: list[str],
|
||||||
|
can_ack: bool,
|
||||||
|
title_template: Template | None,
|
||||||
|
data: dict[Any, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the alert."""
|
||||||
|
self.hass = hass
|
||||||
|
self._attr_name = name
|
||||||
|
self._alert_state = state
|
||||||
|
self._skip_first = skip_first
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
self._message_template = message_template
|
||||||
|
self._done_message_template = done_message_template
|
||||||
|
self._title_template = title_template
|
||||||
|
|
||||||
|
self._notifiers = notifiers
|
||||||
|
self._can_ack = can_ack
|
||||||
|
|
||||||
|
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||||
|
self._next_delay = 0
|
||||||
|
|
||||||
|
self._firing = False
|
||||||
|
self._ack = False
|
||||||
|
self._cancel: Callable[[], None] | None = None
|
||||||
|
self._send_done_message = False
|
||||||
|
self.entity_id = f"{DOMAIN}.{entity_id}"
|
||||||
|
|
||||||
|
async_track_state_change_event(
|
||||||
|
hass, [watched_entity_id], self.watched_entity_change
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return the alert status."""
|
||||||
|
if self._firing:
|
||||||
|
if self._ack:
|
||||||
|
return STATE_OFF
|
||||||
|
return STATE_ON
|
||||||
|
return STATE_IDLE
|
||||||
|
|
||||||
|
async def watched_entity_change(self, event: Event[EventStateChangedData]) -> None:
|
||||||
|
"""Determine if the alert should start or stop."""
|
||||||
|
if (to_state := event.data["new_state"]) is None:
|
||||||
|
return
|
||||||
|
LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"])
|
||||||
|
if to_state.state == self._alert_state and not self._firing:
|
||||||
|
await self.begin_alerting()
|
||||||
|
if to_state.state != self._alert_state and self._firing:
|
||||||
|
await self.end_alerting()
|
||||||
|
|
||||||
|
async def begin_alerting(self) -> None:
|
||||||
|
"""Begin the alert procedures."""
|
||||||
|
LOGGER.debug("Beginning Alert: %s", self._attr_name)
|
||||||
|
self._ack = False
|
||||||
|
self._firing = True
|
||||||
|
self._next_delay = 0
|
||||||
|
|
||||||
|
if not self._skip_first:
|
||||||
|
await self._notify()
|
||||||
|
else:
|
||||||
|
await self._schedule_notify()
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def end_alerting(self) -> None:
|
||||||
|
"""End the alert procedures."""
|
||||||
|
LOGGER.debug("Ending Alert: %s", self._attr_name)
|
||||||
|
if self._cancel is not None:
|
||||||
|
self._cancel()
|
||||||
|
self._cancel = None
|
||||||
|
|
||||||
|
self._ack = False
|
||||||
|
self._firing = False
|
||||||
|
if self._send_done_message:
|
||||||
|
await self._notify_done_message()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def _schedule_notify(self) -> None:
|
||||||
|
"""Schedule a notification."""
|
||||||
|
delay = self._delay[self._next_delay]
|
||||||
|
next_msg = now() + delay
|
||||||
|
self._cancel = async_track_point_in_time(
|
||||||
|
self.hass,
|
||||||
|
HassJob(
|
||||||
|
self._notify, name="Schedule notify alert", cancel_on_shutdown=True
|
||||||
|
),
|
||||||
|
next_msg,
|
||||||
|
)
|
||||||
|
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||||
|
|
||||||
|
async def _notify(self, *args: Any) -> None:
|
||||||
|
"""Send the alert notification."""
|
||||||
|
if not self._firing:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._ack:
|
||||||
|
LOGGER.info("Alerting: %s", self._attr_name)
|
||||||
|
self._send_done_message = True
|
||||||
|
|
||||||
|
if self._message_template is not None:
|
||||||
|
message = self._message_template.async_render(parse_result=False)
|
||||||
|
else:
|
||||||
|
message = self._attr_name
|
||||||
|
|
||||||
|
await self._send_notification_message(message)
|
||||||
|
await self._schedule_notify()
|
||||||
|
|
||||||
|
async def _notify_done_message(self) -> None:
|
||||||
|
"""Send notification of complete alert."""
|
||||||
|
LOGGER.info("Alerting: %s", self._done_message_template)
|
||||||
|
self._send_done_message = False
|
||||||
|
|
||||||
|
if self._done_message_template is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
message = self._done_message_template.async_render(parse_result=False)
|
||||||
|
|
||||||
|
await self._send_notification_message(message)
|
||||||
|
|
||||||
|
async def _send_notification_message(self, message: Any) -> None:
|
||||||
|
if not self._notifiers:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_payload = {ATTR_MESSAGE: message}
|
||||||
|
|
||||||
|
if self._title_template is not None:
|
||||||
|
title = self._title_template.async_render(parse_result=False)
|
||||||
|
msg_payload[ATTR_TITLE] = title
|
||||||
|
if self._data:
|
||||||
|
msg_payload[ATTR_DATA] = self._data
|
||||||
|
|
||||||
|
LOGGER.debug(msg_payload)
|
||||||
|
|
||||||
|
for target in self._notifiers:
|
||||||
|
try:
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
DOMAIN_NOTIFY, target, msg_payload, context=self._context
|
||||||
|
)
|
||||||
|
except ServiceNotFound:
|
||||||
|
LOGGER.error(
|
||||||
|
"Failed to call notify.%s, retrying at next notification interval",
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Async Unacknowledge alert."""
|
||||||
|
LOGGER.debug("Reset Alert: %s", self._attr_name)
|
||||||
|
self._ack = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Async Acknowledge alert."""
|
||||||
|
LOGGER.debug("Acknowledged Alert: %s", self._attr_name)
|
||||||
|
self._ack = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_toggle(self, **kwargs: Any) -> None:
|
||||||
|
"""Async toggle alert."""
|
||||||
|
if self._ack:
|
||||||
|
return await self.async_turn_on()
|
||||||
|
return await self.async_turn_off()
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"toggle": {
|
"toggle": "mdi:bell-ring",
|
||||||
"service": "mdi:bell-ring"
|
"turn_off": "mdi:bell-off",
|
||||||
},
|
"turn_on": "mdi:bell-alert"
|
||||||
"turn_off": {
|
|
||||||
"service": "mdi:bell-off"
|
|
||||||
},
|
|
||||||
"turn_on": {
|
|
||||||
"service": "mdi:bell-alert"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ from homeassistant.components.alarm_control_panel import (
|
|||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
from homeassistant.components.lock import LockState
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CODE_FORMAT,
|
ATTR_CODE_FORMAT,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
@@ -41,12 +40,16 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
|
STATE_LOCKED,
|
||||||
|
STATE_LOCKING,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
|
STATE_UNLOCKED,
|
||||||
|
STATE_UNLOCKING,
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfMass,
|
UnitOfMass,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
@@ -497,10 +500,10 @@ class AlexaLockController(AlexaCapability):
|
|||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
# If its unlocking its still locked and not unlocked yet
|
# If its unlocking its still locked and not unlocked yet
|
||||||
if self.entity.state in (LockState.UNLOCKING, LockState.LOCKED):
|
if self.entity.state in (STATE_UNLOCKING, STATE_LOCKED):
|
||||||
return "LOCKED"
|
return "LOCKED"
|
||||||
# If its locking its still unlocked and not locked yet
|
# If its locking its still unlocked and not locked yet
|
||||||
if self.entity.state in (LockState.LOCKING, LockState.UNLOCKED):
|
if self.entity.state in (STATE_LOCKING, STATE_UNLOCKED):
|
||||||
return "UNLOCKED"
|
return "UNLOCKED"
|
||||||
return "JAMMED"
|
return "JAMMED"
|
||||||
|
|
||||||
|
|||||||
@@ -661,12 +661,9 @@ class RemoteCapabilities(AlexaEntity):
|
|||||||
def interfaces(self) -> Generator[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
yield AlexaModeController(
|
||||||
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||||
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
|
)
|
||||||
yield AlexaModeController(
|
|
||||||
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
|
||||||
)
|
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
@@ -115,8 +114,6 @@ def get_engine(
|
|||||||
|
|
||||||
all_voices: dict[str, dict[str, str]] = {}
|
all_voices: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
all_engines: dict[str, set[str]] = defaultdict(set)
|
|
||||||
|
|
||||||
all_voices_req = polly_client.describe_voices()
|
all_voices_req = polly_client.describe_voices()
|
||||||
|
|
||||||
for voice in all_voices_req.get("Voices", []):
|
for voice in all_voices_req.get("Voices", []):
|
||||||
@@ -127,12 +124,8 @@ def get_engine(
|
|||||||
language_code: str | None = voice.get("LanguageCode")
|
language_code: str | None = voice.get("LanguageCode")
|
||||||
if language_code is not None and language_code not in supported_languages:
|
if language_code is not None and language_code not in supported_languages:
|
||||||
supported_languages.append(language_code)
|
supported_languages.append(language_code)
|
||||||
for engine in voice.get("SupportedEngines"):
|
|
||||||
all_engines[engine].add(voice_id)
|
|
||||||
|
|
||||||
return AmazonPollyProvider(
|
return AmazonPollyProvider(polly_client, config, supported_languages, all_voices)
|
||||||
polly_client, config, supported_languages, all_voices, all_engines
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AmazonPollyProvider(Provider):
|
class AmazonPollyProvider(Provider):
|
||||||
@@ -144,16 +137,13 @@ class AmazonPollyProvider(Provider):
|
|||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
supported_languages: list[str],
|
supported_languages: list[str],
|
||||||
all_voices: dict[str, dict[str, str]],
|
all_voices: dict[str, dict[str, str]],
|
||||||
all_engines: dict[str, set[str]],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Amazon Polly provider for TTS."""
|
"""Initialize Amazon Polly provider for TTS."""
|
||||||
self.client = polly_client
|
self.client = polly_client
|
||||||
self.config = config
|
self.config = config
|
||||||
self.supported_langs = supported_languages
|
self.supported_langs = supported_languages
|
||||||
self.all_voices = all_voices
|
self.all_voices = all_voices
|
||||||
self.all_engines = all_engines
|
|
||||||
self.default_voice: str = self.config[CONF_VOICE]
|
self.default_voice: str = self.config[CONF_VOICE]
|
||||||
self.default_engine: str = self.config[CONF_ENGINE]
|
|
||||||
self.name = "Amazon Polly"
|
self.name = "Amazon Polly"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -169,12 +159,12 @@ class AmazonPollyProvider(Provider):
|
|||||||
@property
|
@property
|
||||||
def default_options(self) -> dict[str, str]:
|
def default_options(self) -> dict[str, str]:
|
||||||
"""Return dict include default options."""
|
"""Return dict include default options."""
|
||||||
return {CONF_VOICE: self.default_voice, CONF_ENGINE: self.default_engine}
|
return {CONF_VOICE: self.default_voice}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_options(self) -> list[str]:
|
def supported_options(self) -> list[str]:
|
||||||
"""Return a list of supported options."""
|
"""Return a list of supported options."""
|
||||||
return [CONF_VOICE, CONF_ENGINE]
|
return [CONF_VOICE]
|
||||||
|
|
||||||
def get_tts_audio(
|
def get_tts_audio(
|
||||||
self,
|
self,
|
||||||
@@ -189,14 +179,9 @@ class AmazonPollyProvider(Provider):
|
|||||||
_LOGGER.error("%s does not support the %s language", voice_id, language)
|
_LOGGER.error("%s does not support the %s language", voice_id, language)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
engine = options.get(CONF_ENGINE, self.default_engine)
|
|
||||||
if voice_id not in self.all_engines[engine]:
|
|
||||||
_LOGGER.error("%s does not support the %s engine", voice_id, engine)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
_LOGGER.debug("Requesting TTS file for text: %s", message)
|
_LOGGER.debug("Requesting TTS file for text: %s", message)
|
||||||
resp = self.client.synthesize_speech(
|
resp = self.client.synthesize_speech(
|
||||||
Engine=engine,
|
Engine=self.config[CONF_ENGINE],
|
||||||
OutputFormat=self.config[CONF_OUTPUT_FORMAT],
|
OutputFormat=self.config[CONF_OUTPUT_FORMAT],
|
||||||
SampleRate=self.config[CONF_SAMPLE_RATE],
|
SampleRate=self.config[CONF_SAMPLE_RATE],
|
||||||
Text=message,
|
Text=message,
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import CONF_SITE_ID, PLATFORMS
|
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||||
from .coordinator import AmberUpdateCoordinator
|
from .coordinator import AmberUpdateCoordinator
|
||||||
|
|
||||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
|
|
||||||
"""Set up Amber Electric from a config entry."""
|
"""Set up Amber Electric from a config entry."""
|
||||||
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
|
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
|
||||||
api_instance = amber_api.AmberApi.create(configuration)
|
api_instance = amber_api.AmberApi.create(configuration)
|
||||||
@@ -21,11 +19,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> boo
|
|||||||
|
|
||||||
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
|
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = coordinator
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AmberConfigEntry
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
from .const import ATTRIBUTION
|
|
||||||
from .coordinator import AmberUpdateCoordinator
|
from .coordinator import AmberUpdateCoordinator
|
||||||
|
|
||||||
PRICE_SPIKE_ICONS = {
|
PRICE_SPIKE_ICONS = {
|
||||||
@@ -85,11 +85,11 @@ class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AmberConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
price_spike_description = BinarySensorEntityDescription(
|
price_spike_description = BinarySensorEntityDescription(
|
||||||
key="price_spike",
|
key="price_spike",
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy
|
from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AmberConfigEntry
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
from .const import ATTRIBUTION
|
|
||||||
from .coordinator import AmberUpdateCoordinator, normalize_descriptor
|
from .coordinator import AmberUpdateCoordinator, normalize_descriptor
|
||||||
|
|
||||||
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
||||||
@@ -196,11 +196,11 @@ class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity):
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AmberConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
current: dict[str, CurrentInterval] = coordinator.data["current"]
|
current: dict[str, CurrentInterval] = coordinator.data["current"]
|
||||||
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]
|
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]
|
||||||
|
|||||||
@@ -8,30 +8,28 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
||||||
|
|
||||||
type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator]
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Set up the Ambient Weather Network from a config entry."""
|
"""Set up the Ambient Weather Network from a config entry."""
|
||||||
|
|
||||||
api = OpenAPI()
|
api = OpenAPI()
|
||||||
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
|
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = coordinator
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioambient"],
|
"loggers": ["aioambient"],
|
||||||
"requirements": ["aioambient==2024.08.0"]
|
"requirements": ["aioambient==2024.01.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
@@ -28,7 +29,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import AmbientNetworkConfigEntry
|
from .const import DOMAIN
|
||||||
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
||||||
from .entity import AmbientNetworkEntity
|
from .entity import AmbientNetworkEntity
|
||||||
|
|
||||||
@@ -270,12 +271,12 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AmbientNetworkConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Ambient Network sensor entities."""
|
"""Set up the Ambient Network sensor entities."""
|
||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
if coordinator.config_entry is not None:
|
if coordinator.config_entry is not None:
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AmbientNetworkSensor(
|
AmbientNetworkSensor(
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioambient"],
|
"loggers": ["aioambient"],
|
||||||
"requirements": ["aioambient==2024.08.0"]
|
"requirements": ["aioambient==2024.01.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,15 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"enable_recording": {
|
"enable_recording": "mdi:record-rec",
|
||||||
"service": "mdi:record-rec"
|
"disable_recording": "mdi:stop",
|
||||||
},
|
"enable_audio": "mdi:volume-high",
|
||||||
"disable_recording": {
|
"disable_audio": "mdi:volume-off",
|
||||||
"service": "mdi:stop"
|
"enable_motion_recording": "mdi:motion-sensor",
|
||||||
},
|
"disable_motion_recording": "mdi:motion-sensor-off",
|
||||||
"enable_audio": {
|
"goto_preset": "mdi:pan",
|
||||||
"service": "mdi:volume-high"
|
"set_color_bw": "mdi:palette",
|
||||||
},
|
"start_tour": "mdi:panorama",
|
||||||
"disable_audio": {
|
"stop_tour": "mdi:panorama-outline",
|
||||||
"service": "mdi:volume-off"
|
"ptz_control": "mdi:pan"
|
||||||
},
|
|
||||||
"enable_motion_recording": {
|
|
||||||
"service": "mdi:motion-sensor"
|
|
||||||
},
|
|
||||||
"disable_motion_recording": {
|
|
||||||
"service": "mdi:motion-sensor-off"
|
|
||||||
},
|
|
||||||
"goto_preset": {
|
|
||||||
"service": "mdi:pan"
|
|
||||||
},
|
|
||||||
"set_color_bw": {
|
|
||||||
"service": "mdi:palette"
|
|
||||||
},
|
|
||||||
"start_tour": {
|
|
||||||
"service": "mdi:panorama"
|
|
||||||
},
|
|
||||||
"stop_tour": {
|
|
||||||
"service": "mdi:panorama-outline"
|
|
||||||
},
|
|
||||||
"ptz_control": {
|
|
||||||
"service": "mdi:pan"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user