mirror of
https://github.com/home-assistant/core.git
synced 2025-08-01 17:48:26 +00:00
Merge branch 'dev' into block_pyserial_asyncio
This commit is contained in:
commit
e7e42dc318
@ -49,6 +49,7 @@ base_platforms: &base_platforms
|
|||||||
- homeassistant/components/tts/**
|
- homeassistant/components/tts/**
|
||||||
- homeassistant/components/update/**
|
- homeassistant/components/update/**
|
||||||
- homeassistant/components/vacuum/**
|
- homeassistant/components/vacuum/**
|
||||||
|
- homeassistant/components/valve/**
|
||||||
- homeassistant/components/water_heater/**
|
- homeassistant/components/water_heater/**
|
||||||
- homeassistant/components/weather/**
|
- homeassistant/components/weather/**
|
||||||
|
|
||||||
@ -145,6 +146,7 @@ requirements: &requirements
|
|||||||
- homeassistant/package_constraints.txt
|
- homeassistant/package_constraints.txt
|
||||||
- requirements*.txt
|
- requirements*.txt
|
||||||
- pyproject.toml
|
- pyproject.toml
|
||||||
|
- script/licenses.py
|
||||||
|
|
||||||
any:
|
any:
|
||||||
- *base_platforms
|
- *base_platforms
|
||||||
|
1738
.coveragerc
1738
.coveragerc
File diff suppressed because it is too large
Load Diff
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -74,7 +74,6 @@ If the code communicates with devices, web services, or third-party tools:
|
|||||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||||
Updated by running `python3 -m script.gen_requirements_all`.
|
Updated by running `python3 -m script.gen_requirements_all`.
|
||||||
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
||||||
- [ ] Untested files have been added to `.coveragerc`.
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
This project is very active and we have a high turnover of pull requests.
|
This project is very active and we have a high turnover of pull requests.
|
||||||
|
26
.github/workflows/builder.yml
vendored
26
.github/workflows/builder.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ jobs:
|
|||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@ -190,14 +190,14 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2024.03.5
|
uses: home-assistant/builder@2024.08.1
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@ -256,14 +256,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2024.03.5
|
uses: home-assistant/builder@2024.08.1
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@ -323,20 +323,20 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.5.0
|
uses: sigstore/cosign-installer@v3.6.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@v3.2.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@ -453,12 +453,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
|
170
.github/workflows/ci.yaml
vendored
170
.github/workflows/ci.yaml
vendored
@ -31,12 +31,16 @@ on:
|
|||||||
description: "Only run mypy"
|
description: "Only run mypy"
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
audit-licenses-only:
|
||||||
|
description: "Only run audit licenses"
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 9
|
CACHE_VERSION: 10
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 8
|
MYPY_CACHE_VERSION: 8
|
||||||
HA_SHORT_VERSION: "2024.7"
|
HA_SHORT_VERSION: "2024.9"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.12"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@ -86,7 +90,7 @@ jobs:
|
|||||||
tests_glob: ${{ steps.info.outputs.tests_glob }}
|
tests_glob: ${{ steps.info.outputs.tests_glob }}
|
||||||
tests: ${{ steps.info.outputs.tests }}
|
tests: ${{ steps.info.outputs.tests }}
|
||||||
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
@ -218,10 +222,11 @@ jobs:
|
|||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
name: Prepare pre-commit base
|
name: Prepare pre-commit base
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.inputs.pylint-only != 'true'
|
github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
@ -229,7 +234,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -266,7 +271,7 @@ jobs:
|
|||||||
|
|
||||||
lint-ruff-format:
|
lint-ruff-format:
|
||||||
name: Check ruff-format
|
name: Check ruff-format
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- pre-commit
|
- pre-commit
|
||||||
@ -274,7 +279,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -306,7 +311,7 @@ jobs:
|
|||||||
|
|
||||||
lint-ruff:
|
lint-ruff:
|
||||||
name: Check ruff
|
name: Check ruff
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- pre-commit
|
- pre-commit
|
||||||
@ -314,7 +319,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -343,9 +348,10 @@ jobs:
|
|||||||
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
|
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
|
||||||
env:
|
env:
|
||||||
RUFF_OUTPUT_FORMAT: github
|
RUFF_OUTPUT_FORMAT: github
|
||||||
|
|
||||||
lint-other:
|
lint-other:
|
||||||
name: Check other linters
|
name: Check other linters
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- pre-commit
|
- pre-commit
|
||||||
@ -353,7 +359,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
@ -437,7 +443,7 @@ jobs:
|
|||||||
|
|
||||||
base:
|
base:
|
||||||
name: Prepare dependencies
|
name: Prepare dependencies
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs: info
|
needs: info
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
strategy:
|
strategy:
|
||||||
@ -448,7 +454,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -488,6 +494,7 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
libturbojpeg \
|
||||||
libavcodec-dev \
|
libavcodec-dev \
|
||||||
libavdevice-dev \
|
libavdevice-dev \
|
||||||
libavfilter-dev \
|
libavfilter-dev \
|
||||||
@ -507,25 +514,31 @@ jobs:
|
|||||||
uv pip install -U "pip>=21.3.1" setuptools wheel
|
uv pip install -U "pip>=21.3.1" setuptools wheel
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
uv pip install -r requirements_all_pytest.txt
|
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
|
||||||
uv pip install -r requirements_test.txt
|
|
||||||
uv pip install -e . --config-settings editable_mode=compat
|
uv pip install -e . --config-settings editable_mode=compat
|
||||||
|
|
||||||
hassfest:
|
hassfest:
|
||||||
name: Check hassfest
|
name: Check hassfest
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.inputs.pylint-only != 'true'
|
github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install additional OS dependencies
|
||||||
|
run: |
|
||||||
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install \
|
||||||
|
libturbojpeg
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -545,10 +558,11 @@ jobs:
|
|||||||
|
|
||||||
gen-requirements-all:
|
gen-requirements-all:
|
||||||
name: Check all requirements
|
name: Check all requirements
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.inputs.pylint-only != 'true'
|
github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
@ -557,7 +571,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -575,12 +589,56 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.gen_requirements_all validate
|
python -m script.gen_requirements_all validate
|
||||||
|
|
||||||
|
audit-licenses:
|
||||||
|
name: Audit licenses
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- info
|
||||||
|
- base
|
||||||
|
if: |
|
||||||
|
(github.event.inputs.pylint-only != 'true'
|
||||||
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
|| github.event.inputs.audit-licenses-only == 'true')
|
||||||
|
&& needs.info.outputs.requirements == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v5.1.1
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache/restore@v4.0.2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
|
needs.info.outputs.python_cache_key }}
|
||||||
|
- name: Run pip-licenses
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pip-licenses --format=json --output-file=licenses.json
|
||||||
|
- name: Upload licenses
|
||||||
|
uses: actions/upload-artifact@v4.3.6
|
||||||
|
with:
|
||||||
|
name: licenses
|
||||||
|
path: licenses.json
|
||||||
|
- name: Process licenses
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
python -m script.licenses
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
if: |
|
if: |
|
||||||
github.event.inputs.mypy-only != 'true'
|
github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
|| github.event.inputs.pylint-only == 'true'
|
|| github.event.inputs.pylint-only == 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -590,7 +648,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -622,10 +680,12 @@ jobs:
|
|||||||
|
|
||||||
pylint-tests:
|
pylint-tests:
|
||||||
name: Check pylint on tests
|
name: Check pylint on tests
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
if: |
|
if: |
|
||||||
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true')
|
(github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
|
|| github.event.inputs.pylint-only == 'true')
|
||||||
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
|
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -635,7 +695,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -667,9 +727,10 @@ jobs:
|
|||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
name: Check mypy
|
name: Check mypy
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.inputs.pylint-only != 'true'
|
github.event.inputs.pylint-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
|| github.event.inputs.mypy-only == 'true'
|
|| github.event.inputs.mypy-only == 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -679,7 +740,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -728,12 +789,13 @@ jobs:
|
|||||||
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||||
|
|
||||||
prepare-pytest-full:
|
prepare-pytest-full:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||||
&& github.event.inputs.lint-only != 'true'
|
&& github.event.inputs.lint-only != 'true'
|
||||||
&& github.event.inputs.pylint-only != 'true'
|
&& github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
&& needs.info.outputs.test_full_suite == 'true'
|
&& needs.info.outputs.test_full_suite == 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -747,12 +809,13 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -770,19 +833,20 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
pytest-full:
|
pytest-full:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||||
&& github.event.inputs.lint-only != 'true'
|
&& github.event.inputs.lint-only != 'true'
|
||||||
&& github.event.inputs.pylint-only != 'true'
|
&& github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
&& needs.info.outputs.test_full_suite == 'true'
|
&& needs.info.outputs.test_full_suite == 'true'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -809,12 +873,13 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -833,7 +898,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
@ -855,6 +920,7 @@ jobs:
|
|||||||
cov_params+=(--cov-report=xml)
|
cov_params+=(--cov-report=xml)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
|
||||||
python3 -b -X dev -m pytest \
|
python3 -b -X dev -m pytest \
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
@ -868,14 +934,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -887,7 +953,7 @@ jobs:
|
|||||||
./script/check_dirty
|
./script/check_dirty
|
||||||
|
|
||||||
pytest-mariadb:
|
pytest-mariadb:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
services:
|
services:
|
||||||
mariadb:
|
mariadb:
|
||||||
image: ${{ matrix.mariadb-group }}
|
image: ${{ matrix.mariadb-group }}
|
||||||
@ -901,6 +967,7 @@ jobs:
|
|||||||
&& github.event.inputs.lint-only != 'true'
|
&& github.event.inputs.lint-only != 'true'
|
||||||
&& github.event.inputs.pylint-only != 'true'
|
&& github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
&& needs.info.outputs.mariadb_groups != '[]'
|
&& needs.info.outputs.mariadb_groups != '[]'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -926,12 +993,13 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
libturbojpeg \
|
||||||
libmariadb-dev-compat
|
libmariadb-dev-compat
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -992,7 +1060,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1000,7 +1068,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1025,6 +1093,7 @@ jobs:
|
|||||||
&& github.event.inputs.lint-only != 'true'
|
&& github.event.inputs.lint-only != 'true'
|
||||||
&& github.event.inputs.pylint-only != 'true'
|
&& github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
&& needs.info.outputs.postgresql_groups != '[]'
|
&& needs.info.outputs.postgresql_groups != '[]'
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
@ -1050,12 +1119,13 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
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.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -1117,7 +1187,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1125,7 +1195,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1138,7 +1208,7 @@ jobs:
|
|||||||
coverage-full:
|
coverage-full:
|
||||||
name: Upload test coverage to Codecov (full suite)
|
name: Upload test coverage to Codecov (full suite)
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- pytest-full
|
- pytest-full
|
||||||
@ -1149,7 +1219,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
@ -1162,12 +1232,13 @@ jobs:
|
|||||||
version: v0.6.0
|
version: v0.6.0
|
||||||
|
|
||||||
pytest-partial:
|
pytest-partial:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||||
&& github.event.inputs.lint-only != 'true'
|
&& github.event.inputs.lint-only != 'true'
|
||||||
&& github.event.inputs.pylint-only != 'true'
|
&& github.event.inputs.pylint-only != 'true'
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
&& needs.info.outputs.tests_glob
|
&& needs.info.outputs.tests_glob
|
||||||
&& needs.info.outputs.test_full_suite == 'false'
|
&& needs.info.outputs.test_full_suite == 'false'
|
||||||
needs:
|
needs:
|
||||||
@ -1194,12 +1265,13 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -1257,14 +1329,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -1276,7 +1348,7 @@ jobs:
|
|||||||
coverage-partial:
|
coverage-partial:
|
||||||
name: Upload test coverage to Codecov (partial suite)
|
name: Upload test coverage to Codecov (partial suite)
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- pytest-partial
|
- pytest-partial
|
||||||
@ -1285,7 +1357,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.25.10
|
uses: github/codeql-action/init@v3.26.2
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.25.10
|
uses: github/codeql-action/analyze@v3.26.2
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
36
.github/workflows/wheels.yml
vendored
36
.github/workflows/wheels.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -82,14 +82,14 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@ -101,7 +101,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.6
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@ -121,17 +121,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -159,17 +159,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ jobs:
|
|||||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
- name: Build wheels (old cython)
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -211,14 +211,14 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_old-cython.txt"
|
requirements: "requirements_old-cython.txt"
|
||||||
pip: "'cython<3'"
|
pip: "'cython<3'"
|
||||||
|
|
||||||
- name: Build wheels (part 1)
|
- name: Build wheels (part 1)
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -226,13 +226,13 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtaa"
|
requirements: "requirements_all.txtaa"
|
||||||
|
|
||||||
- name: Build wheels (part 2)
|
- name: Build wheels (part 2)
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -240,13 +240,13 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtab"
|
requirements: "requirements_all.txtab"
|
||||||
|
|
||||||
- name: Build wheels (part 3)
|
- name: Build wheels (part 3)
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@ -254,7 +254,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtac"
|
requirements: "requirements_all.txtac"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.4.9
|
rev: v0.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -12,7 +12,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue
|
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json, html]
|
||||||
@ -83,7 +83,7 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
|
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
|
||||||
- id: hassfest-metadata
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||||
|
@ -21,6 +21,7 @@ homeassistant.helpers.entity_platform
|
|||||||
homeassistant.helpers.entity_values
|
homeassistant.helpers.entity_values
|
||||||
homeassistant.helpers.event
|
homeassistant.helpers.event
|
||||||
homeassistant.helpers.reload
|
homeassistant.helpers.reload
|
||||||
|
homeassistant.helpers.script
|
||||||
homeassistant.helpers.script_variables
|
homeassistant.helpers.script_variables
|
||||||
homeassistant.helpers.singleton
|
homeassistant.helpers.singleton
|
||||||
homeassistant.helpers.sun
|
homeassistant.helpers.sun
|
||||||
@ -94,9 +95,8 @@ homeassistant.components.aruba.*
|
|||||||
homeassistant.components.arwn.*
|
homeassistant.components.arwn.*
|
||||||
homeassistant.components.aseko_pool_live.*
|
homeassistant.components.aseko_pool_live.*
|
||||||
homeassistant.components.assist_pipeline.*
|
homeassistant.components.assist_pipeline.*
|
||||||
homeassistant.components.asterisk_cdr.*
|
|
||||||
homeassistant.components.asterisk_mbox.*
|
|
||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
|
homeassistant.components.autarco.*
|
||||||
homeassistant.components.auth.*
|
homeassistant.components.auth.*
|
||||||
homeassistant.components.automation.*
|
homeassistant.components.automation.*
|
||||||
homeassistant.components.awair.*
|
homeassistant.components.awair.*
|
||||||
@ -118,6 +118,7 @@ homeassistant.components.bond.*
|
|||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
homeassistant.components.brother.*
|
homeassistant.components.brother.*
|
||||||
homeassistant.components.browser.*
|
homeassistant.components.browser.*
|
||||||
|
homeassistant.components.bryant_evolution.*
|
||||||
homeassistant.components.bthome.*
|
homeassistant.components.bthome.*
|
||||||
homeassistant.components.button.*
|
homeassistant.components.button.*
|
||||||
homeassistant.components.calendar.*
|
homeassistant.components.calendar.*
|
||||||
@ -165,6 +166,7 @@ homeassistant.components.ecowitt.*
|
|||||||
homeassistant.components.efergy.*
|
homeassistant.components.efergy.*
|
||||||
homeassistant.components.electrasmart.*
|
homeassistant.components.electrasmart.*
|
||||||
homeassistant.components.electric_kiwi.*
|
homeassistant.components.electric_kiwi.*
|
||||||
|
homeassistant.components.elevenlabs.*
|
||||||
homeassistant.components.elgato.*
|
homeassistant.components.elgato.*
|
||||||
homeassistant.components.elkm1.*
|
homeassistant.components.elkm1.*
|
||||||
homeassistant.components.emulated_hue.*
|
homeassistant.components.emulated_hue.*
|
||||||
@ -195,6 +197,7 @@ homeassistant.components.fritzbox_callmonitor.*
|
|||||||
homeassistant.components.fronius.*
|
homeassistant.components.fronius.*
|
||||||
homeassistant.components.frontend.*
|
homeassistant.components.frontend.*
|
||||||
homeassistant.components.fully_kiosk.*
|
homeassistant.components.fully_kiosk.*
|
||||||
|
homeassistant.components.fyta.*
|
||||||
homeassistant.components.generic_hygrostat.*
|
homeassistant.components.generic_hygrostat.*
|
||||||
homeassistant.components.generic_thermostat.*
|
homeassistant.components.generic_thermostat.*
|
||||||
homeassistant.components.geo_location.*
|
homeassistant.components.geo_location.*
|
||||||
@ -253,6 +256,7 @@ homeassistant.components.integration.*
|
|||||||
homeassistant.components.intent.*
|
homeassistant.components.intent.*
|
||||||
homeassistant.components.intent_script.*
|
homeassistant.components.intent_script.*
|
||||||
homeassistant.components.ios.*
|
homeassistant.components.ios.*
|
||||||
|
homeassistant.components.iotty.*
|
||||||
homeassistant.components.ipp.*
|
homeassistant.components.ipp.*
|
||||||
homeassistant.components.iqvia.*
|
homeassistant.components.iqvia.*
|
||||||
homeassistant.components.islamic_prayer_times.*
|
homeassistant.components.islamic_prayer_times.*
|
||||||
@ -277,6 +281,7 @@ homeassistant.components.lidarr.*
|
|||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
homeassistant.components.linear_garage_door.*
|
homeassistant.components.linear_garage_door.*
|
||||||
|
homeassistant.components.linkplay.*
|
||||||
homeassistant.components.litejet.*
|
homeassistant.components.litejet.*
|
||||||
homeassistant.components.litterrobot.*
|
homeassistant.components.litterrobot.*
|
||||||
homeassistant.components.local_ip.*
|
homeassistant.components.local_ip.*
|
||||||
@ -287,6 +292,7 @@ homeassistant.components.logger.*
|
|||||||
homeassistant.components.london_underground.*
|
homeassistant.components.london_underground.*
|
||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.mailbox.*
|
homeassistant.components.mailbox.*
|
||||||
homeassistant.components.map.*
|
homeassistant.components.map.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
@ -370,6 +376,7 @@ homeassistant.components.rhasspy.*
|
|||||||
homeassistant.components.ridwell.*
|
homeassistant.components.ridwell.*
|
||||||
homeassistant.components.ring.*
|
homeassistant.components.ring.*
|
||||||
homeassistant.components.rituals_perfume_genie.*
|
homeassistant.components.rituals_perfume_genie.*
|
||||||
|
homeassistant.components.roborock.*
|
||||||
homeassistant.components.roku.*
|
homeassistant.components.roku.*
|
||||||
homeassistant.components.romy.*
|
homeassistant.components.romy.*
|
||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
@ -381,6 +388,7 @@ homeassistant.components.samsungtv.*
|
|||||||
homeassistant.components.scene.*
|
homeassistant.components.scene.*
|
||||||
homeassistant.components.schedule.*
|
homeassistant.components.schedule.*
|
||||||
homeassistant.components.scrape.*
|
homeassistant.components.scrape.*
|
||||||
|
homeassistant.components.script.*
|
||||||
homeassistant.components.search.*
|
homeassistant.components.search.*
|
||||||
homeassistant.components.select.*
|
homeassistant.components.select.*
|
||||||
homeassistant.components.sensibo.*
|
homeassistant.components.sensibo.*
|
||||||
|
42
.vscode/launch.json
vendored
42
.vscode/launch.json
vendored
@ -6,38 +6,52 @@
|
|||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Home Assistant",
|
"name": "Home Assistant",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "homeassistant",
|
"module": "homeassistant",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": ["--debug", "-c", "config"],
|
"args": [
|
||||||
|
"--debug",
|
||||||
|
"-c",
|
||||||
|
"config"
|
||||||
|
],
|
||||||
"preLaunchTask": "Compile English translations"
|
"preLaunchTask": "Compile English translations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Home Assistant (skip pip)",
|
"name": "Home Assistant (skip pip)",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "homeassistant",
|
"module": "homeassistant",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": ["--debug", "-c", "config", "--skip-pip"],
|
"args": [
|
||||||
|
"--debug",
|
||||||
|
"-c",
|
||||||
|
"config",
|
||||||
|
"--skip-pip"
|
||||||
|
],
|
||||||
"preLaunchTask": "Compile English translations"
|
"preLaunchTask": "Compile English translations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Home Assistant: Changed tests",
|
"name": "Home Assistant: Changed tests",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "pytest",
|
"module": "pytest",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": ["--timeout=10", "--picked"],
|
"args": [
|
||||||
|
"--timeout=10",
|
||||||
|
"--picked"
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||||
// See https://www.home-assistant.io/integrations/debugpy/
|
// See https://www.home-assistant.io/integrations/debugpy/
|
||||||
"name": "Home Assistant: Attach Local",
|
"name": "Home Assistant: Attach Local",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"port": 5678,
|
"connect": {
|
||||||
"host": "localhost",
|
"port": 5678,
|
||||||
|
"host": "localhost"
|
||||||
|
},
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
@ -49,10 +63,12 @@
|
|||||||
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
|
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
|
||||||
// See https://www.home-assistant.io/integrations/debugpy/
|
// See https://www.home-assistant.io/integrations/debugpy/
|
||||||
"name": "Home Assistant: Attach Remote",
|
"name": "Home Assistant: Attach Remote",
|
||||||
"type": "python",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"port": 5678,
|
"connect": {
|
||||||
"host": "homeassistant.local",
|
"port": 5678,
|
||||||
|
"host": "homeassistant.local"
|
||||||
|
},
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
@ -61,4 +77,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
1
.vscode/tasks.json
vendored
1
.vscode/tasks.json
vendored
@ -76,6 +76,7 @@
|
|||||||
"detail": "Generate code coverage report for a given integration.",
|
"detail": "Generate code coverage report for a given integration.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
||||||
|
"dependsOn": ["Compile English translations"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
86
CODEOWNERS
86
CODEOWNERS
@ -80,8 +80,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airzone/ @Noltari
|
/tests/components/airzone/ @Noltari
|
||||||
/homeassistant/components/airzone_cloud/ @Noltari
|
/homeassistant/components/airzone_cloud/ @Noltari
|
||||||
/tests/components/airzone_cloud/ @Noltari
|
/tests/components/airzone_cloud/ @Noltari
|
||||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
|
||||||
/tests/components/aladdin_connect/ @swcloudgenie
|
|
||||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||||
@ -110,6 +108,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/anova/ @Lash-L
|
/tests/components/anova/ @Lash-L
|
||||||
/homeassistant/components/anthemav/ @hyralex
|
/homeassistant/components/anthemav/ @hyralex
|
||||||
/tests/components/anthemav/ @hyralex
|
/tests/components/anthemav/ @hyralex
|
||||||
|
/homeassistant/components/anthropic/ @Shulyaka
|
||||||
|
/tests/components/anthropic/ @Shulyaka
|
||||||
/homeassistant/components/aosmith/ @bdr99
|
/homeassistant/components/aosmith/ @bdr99
|
||||||
/tests/components/aosmith/ @bdr99
|
/tests/components/aosmith/ @bdr99
|
||||||
/homeassistant/components/apache_kafka/ @bachya
|
/homeassistant/components/apache_kafka/ @bachya
|
||||||
@ -157,6 +157,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/aurora_abb_powerone/ @davet2001
|
/tests/components/aurora_abb_powerone/ @davet2001
|
||||||
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
||||||
/tests/components/aussie_broadband/ @nickw444 @Bre77
|
/tests/components/aussie_broadband/ @nickw444 @Bre77
|
||||||
|
/homeassistant/components/autarco/ @klaasnicolaas
|
||||||
|
/tests/components/autarco/ @klaasnicolaas
|
||||||
/homeassistant/components/auth/ @home-assistant/core
|
/homeassistant/components/auth/ @home-assistant/core
|
||||||
/tests/components/auth/ @home-assistant/core
|
/tests/components/auth/ @home-assistant/core
|
||||||
/homeassistant/components/automation/ @home-assistant/core
|
/homeassistant/components/automation/ @home-assistant/core
|
||||||
@ -197,7 +199,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/bluemaestro/ @bdraco
|
/tests/components/bluemaestro/ @bdraco
|
||||||
/homeassistant/components/blueprint/ @home-assistant/core
|
/homeassistant/components/blueprint/ @home-assistant/core
|
||||||
/tests/components/blueprint/ @home-assistant/core
|
/tests/components/blueprint/ @home-assistant/core
|
||||||
/homeassistant/components/bluesound/ @thrawnarn
|
/homeassistant/components/bluesound/ @thrawnarn @LouisChrist
|
||||||
|
/tests/components/bluesound/ @thrawnarn @LouisChrist
|
||||||
/homeassistant/components/bluetooth/ @bdraco
|
/homeassistant/components/bluetooth/ @bdraco
|
||||||
/tests/components/bluetooth/ @bdraco
|
/tests/components/bluetooth/ @bdraco
|
||||||
/homeassistant/components/bluetooth_adapters/ @bdraco
|
/homeassistant/components/bluetooth_adapters/ @bdraco
|
||||||
@ -220,6 +223,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/brottsplatskartan/ @gjohansson-ST
|
/tests/components/brottsplatskartan/ @gjohansson-ST
|
||||||
/homeassistant/components/brunt/ @eavanvalkenburg
|
/homeassistant/components/brunt/ @eavanvalkenburg
|
||||||
/tests/components/brunt/ @eavanvalkenburg
|
/tests/components/brunt/ @eavanvalkenburg
|
||||||
|
/homeassistant/components/bryant_evolution/ @danielsmyers
|
||||||
|
/tests/components/bryant_evolution/ @danielsmyers
|
||||||
/homeassistant/components/bsblan/ @liudger
|
/homeassistant/components/bsblan/ @liudger
|
||||||
/tests/components/bsblan/ @liudger
|
/tests/components/bsblan/ @liudger
|
||||||
/homeassistant/components/bt_smarthub/ @typhoon2099
|
/homeassistant/components/bt_smarthub/ @typhoon2099
|
||||||
@ -239,6 +244,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ccm15/ @ocalvo
|
/tests/components/ccm15/ @ocalvo
|
||||||
/homeassistant/components/cert_expiry/ @jjlawren
|
/homeassistant/components/cert_expiry/ @jjlawren
|
||||||
/tests/components/cert_expiry/ @jjlawren
|
/tests/components/cert_expiry/ @jjlawren
|
||||||
|
/homeassistant/components/chacon_dio/ @cnico
|
||||||
|
/tests/components/chacon_dio/ @cnico
|
||||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||||
@ -342,8 +349,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dremel_3d_printer/ @tkdrob
|
/tests/components/dremel_3d_printer/ @tkdrob
|
||||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
/homeassistant/components/dsmr/ @Robbie1221
|
||||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
/tests/components/dsmr/ @Robbie1221
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/homeassistant/components/duotecno/ @cereal2nd
|
/homeassistant/components/duotecno/ @cereal2nd
|
||||||
@ -360,8 +367,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ecoforest/ @pjanuario
|
/tests/components/ecoforest/ @pjanuario
|
||||||
/homeassistant/components/econet/ @w1ll1am23
|
/homeassistant/components/econet/ @w1ll1am23
|
||||||
/tests/components/econet/ @w1ll1am23
|
/tests/components/econet/ @w1ll1am23
|
||||||
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
|
/homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar
|
||||||
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
|
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
|
||||||
/homeassistant/components/ecowitt/ @pvizeli
|
/homeassistant/components/ecowitt/ @pvizeli
|
||||||
/tests/components/ecowitt/ @pvizeli
|
/tests/components/ecowitt/ @pvizeli
|
||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
@ -371,6 +378,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/electrasmart/ @jafar-atili
|
/tests/components/electrasmart/ @jafar-atili
|
||||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||||
/tests/components/electric_kiwi/ @mikey0000
|
/tests/components/electric_kiwi/ @mikey0000
|
||||||
|
/homeassistant/components/elevenlabs/ @sorgfresser
|
||||||
|
/tests/components/elevenlabs/ @sorgfresser
|
||||||
/homeassistant/components/elgato/ @frenck
|
/homeassistant/components/elgato/ @frenck
|
||||||
/tests/components/elgato/ @frenck
|
/tests/components/elgato/ @frenck
|
||||||
/homeassistant/components/elkm1/ @gwww @bdraco
|
/homeassistant/components/elkm1/ @gwww @bdraco
|
||||||
@ -382,6 +391,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/elvia/ @ludeeus
|
/tests/components/elvia/ @ludeeus
|
||||||
/homeassistant/components/emby/ @mezz64
|
/homeassistant/components/emby/ @mezz64
|
||||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||||
|
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||||
/homeassistant/components/emonitor/ @bdraco
|
/homeassistant/components/emonitor/ @bdraco
|
||||||
/tests/components/emonitor/ @bdraco
|
/tests/components/emonitor/ @bdraco
|
||||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||||
@ -398,8 +408,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enocean/ @bdurrer
|
/homeassistant/components/enocean/ @bdurrer
|
||||||
/tests/components/enocean/ @bdurrer
|
/tests/components/enocean/ @bdurrer
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||||
@ -423,6 +433,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/evil_genius_labs/ @balloob
|
/homeassistant/components/evil_genius_labs/ @balloob
|
||||||
/tests/components/evil_genius_labs/ @balloob
|
/tests/components/evil_genius_labs/ @balloob
|
||||||
/homeassistant/components/evohome/ @zxdavb
|
/homeassistant/components/evohome/ @zxdavb
|
||||||
|
/tests/components/evohome/ @zxdavb
|
||||||
/homeassistant/components/ezviz/ @RenierM26 @baqs
|
/homeassistant/components/ezviz/ @RenierM26 @baqs
|
||||||
/tests/components/ezviz/ @RenierM26 @baqs
|
/tests/components/ezviz/ @RenierM26 @baqs
|
||||||
/homeassistant/components/faa_delays/ @ntilley905
|
/homeassistant/components/faa_delays/ @ntilley905
|
||||||
@ -431,6 +442,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/fan/ @home-assistant/core
|
/tests/components/fan/ @home-assistant/core
|
||||||
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
|
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
|
||||||
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
|
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
|
||||||
|
/homeassistant/components/feedreader/ @mib1185
|
||||||
|
/tests/components/feedreader/ @mib1185
|
||||||
/homeassistant/components/fibaro/ @rappenze
|
/homeassistant/components/fibaro/ @rappenze
|
||||||
/tests/components/fibaro/ @rappenze
|
/tests/components/fibaro/ @rappenze
|
||||||
/homeassistant/components/file/ @fabaff
|
/homeassistant/components/file/ @fabaff
|
||||||
@ -501,6 +514,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/generic_hygrostat/ @Shulyaka
|
/homeassistant/components/generic_hygrostat/ @Shulyaka
|
||||||
/tests/components/generic_hygrostat/ @Shulyaka
|
/tests/components/generic_hygrostat/ @Shulyaka
|
||||||
/homeassistant/components/geniushub/ @manzanotti
|
/homeassistant/components/geniushub/ @manzanotti
|
||||||
|
/tests/components/geniushub/ @manzanotti
|
||||||
/homeassistant/components/geo_json_events/ @exxamalte
|
/homeassistant/components/geo_json_events/ @exxamalte
|
||||||
/tests/components/geo_json_events/ @exxamalte
|
/tests/components/geo_json_events/ @exxamalte
|
||||||
/homeassistant/components/geo_location/ @home-assistant/core
|
/homeassistant/components/geo_location/ @home-assistant/core
|
||||||
@ -631,8 +645,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
/tests/components/hvv_departures/ @vigonotion
|
/tests/components/hvv_departures/ @vigonotion
|
||||||
/homeassistant/components/hydrawise/ @dknowles2 @ptcryan
|
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
||||||
/tests/components/hydrawise/ @dknowles2 @ptcryan
|
/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
||||||
/homeassistant/components/hyperion/ @dermotduffy
|
/homeassistant/components/hyperion/ @dermotduffy
|
||||||
/tests/components/hyperion/ @dermotduffy
|
/tests/components/hyperion/ @dermotduffy
|
||||||
/homeassistant/components/ialarm/ @RyuzakiKK
|
/homeassistant/components/ialarm/ @RyuzakiKK
|
||||||
@ -691,6 +705,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ios/ @robbiet480
|
/tests/components/ios/ @robbiet480
|
||||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
|
/homeassistant/components/iotty/ @pburgio
|
||||||
|
/tests/components/iotty/ @pburgio
|
||||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||||
/homeassistant/components/ipma/ @dgomes
|
/homeassistant/components/ipma/ @dgomes
|
||||||
/tests/components/ipma/ @dgomes
|
/tests/components/ipma/ @dgomes
|
||||||
@ -699,10 +715,14 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/iqvia/ @bachya
|
/homeassistant/components/iqvia/ @bachya
|
||||||
/tests/components/iqvia/ @bachya
|
/tests/components/iqvia/ @bachya
|
||||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||||
|
/homeassistant/components/iron_os/ @tr4nt0r
|
||||||
|
/tests/components/iron_os/ @tr4nt0r
|
||||||
/homeassistant/components/isal/ @bdraco
|
/homeassistant/components/isal/ @bdraco
|
||||||
/tests/components/isal/ @bdraco
|
/tests/components/isal/ @bdraco
|
||||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
|
/homeassistant/components/israel_rail/ @shaiu
|
||||||
|
/tests/components/israel_rail/ @shaiu
|
||||||
/homeassistant/components/iss/ @DurgNomis-drol
|
/homeassistant/components/iss/ @DurgNomis-drol
|
||||||
/tests/components/iss/ @DurgNomis-drol
|
/tests/components/iss/ @DurgNomis-drol
|
||||||
/homeassistant/components/ista_ecotrend/ @tr4nt0r
|
/homeassistant/components/ista_ecotrend/ @tr4nt0r
|
||||||
@ -737,8 +757,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/kitchen_sink/ @home-assistant/core
|
/tests/components/kitchen_sink/ @home-assistant/core
|
||||||
/homeassistant/components/kmtronic/ @dgomes
|
/homeassistant/components/kmtronic/ @dgomes
|
||||||
/tests/components/kmtronic/ @dgomes
|
/tests/components/kmtronic/ @dgomes
|
||||||
/homeassistant/components/knocki/ @joostlek @jgatto1
|
/homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
|
||||||
/tests/components/knocki/ @joostlek @jgatto1
|
/tests/components/knocki/ @joostlek @jgatto1 @JakeBosh
|
||||||
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
|
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
|
||||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||||
/homeassistant/components/kodi/ @OnFreund
|
/homeassistant/components/kodi/ @OnFreund
|
||||||
@ -779,10 +799,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
|
/homeassistant/components/lifx/ @Djelibeybi
|
||||||
|
/tests/components/lifx/ @Djelibeybi
|
||||||
/homeassistant/components/light/ @home-assistant/core
|
/homeassistant/components/light/ @home-assistant/core
|
||||||
/tests/components/light/ @home-assistant/core
|
/tests/components/light/ @home-assistant/core
|
||||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||||
/tests/components/linear_garage_door/ @IceBotYT
|
/tests/components/linear_garage_door/ @IceBotYT
|
||||||
|
/homeassistant/components/linkplay/ @Velleman
|
||||||
|
/tests/components/linkplay/ @Velleman
|
||||||
/homeassistant/components/linux_battery/ @fabaff
|
/homeassistant/components/linux_battery/ @fabaff
|
||||||
/homeassistant/components/litejet/ @joncar
|
/homeassistant/components/litejet/ @joncar
|
||||||
/tests/components/litejet/ @joncar
|
/tests/components/litejet/ @joncar
|
||||||
@ -802,8 +826,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/logbook/ @home-assistant/core
|
/tests/components/logbook/ @home-assistant/core
|
||||||
/homeassistant/components/logger/ @home-assistant/core
|
/homeassistant/components/logger/ @home-assistant/core
|
||||||
/tests/components/logger/ @home-assistant/core
|
/tests/components/logger/ @home-assistant/core
|
||||||
/homeassistant/components/logi_circle/ @evanjd
|
|
||||||
/tests/components/logi_circle/ @evanjd
|
|
||||||
/homeassistant/components/london_underground/ @jpbede
|
/homeassistant/components/london_underground/ @jpbede
|
||||||
/tests/components/london_underground/ @jpbede
|
/tests/components/london_underground/ @jpbede
|
||||||
/homeassistant/components/lookin/ @ANMalko @bdraco
|
/homeassistant/components/lookin/ @ANMalko @bdraco
|
||||||
@ -823,13 +845,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
|
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
|
||||||
/homeassistant/components/lyric/ @timmo001
|
/homeassistant/components/lyric/ @timmo001
|
||||||
/tests/components/lyric/ @timmo001
|
/tests/components/lyric/ @timmo001
|
||||||
/homeassistant/components/mastodon/ @fabaff
|
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||||
|
/tests/components/madvr/ @iloveicedgreentea
|
||||||
|
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||||
|
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||||
/homeassistant/components/matrix/ @PaarthShah
|
/homeassistant/components/matrix/ @PaarthShah
|
||||||
/tests/components/matrix/ @PaarthShah
|
/tests/components/matrix/ @PaarthShah
|
||||||
/homeassistant/components/matter/ @home-assistant/matter
|
/homeassistant/components/matter/ @home-assistant/matter
|
||||||
/tests/components/matter/ @home-assistant/matter
|
/tests/components/matter/ @home-assistant/matter
|
||||||
/homeassistant/components/mealie/ @joostlek
|
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
|
||||||
/tests/components/mealie/ @joostlek
|
/tests/components/mealie/ @joostlek @andrew-codechimp
|
||||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||||
/tests/components/meater/ @Sotolotl @emontnemery
|
/tests/components/meater/ @Sotolotl @emontnemery
|
||||||
/homeassistant/components/medcom_ble/ @elafargue
|
/homeassistant/components/medcom_ble/ @elafargue
|
||||||
@ -874,8 +899,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/moat/ @bdraco
|
/tests/components/moat/ @bdraco
|
||||||
/homeassistant/components/mobile_app/ @home-assistant/core
|
/homeassistant/components/mobile_app/ @home-assistant/core
|
||||||
/tests/components/mobile_app/ @home-assistant/core
|
/tests/components/mobile_app/ @home-assistant/core
|
||||||
/homeassistant/components/modbus/ @janiversen
|
|
||||||
/tests/components/modbus/ @janiversen
|
|
||||||
/homeassistant/components/modem_callerid/ @tkdrob
|
/homeassistant/components/modem_callerid/ @tkdrob
|
||||||
/tests/components/modem_callerid/ @tkdrob
|
/tests/components/modem_callerid/ @tkdrob
|
||||||
/homeassistant/components/modern_forms/ @wonderslug
|
/homeassistant/components/modern_forms/ @wonderslug
|
||||||
@ -945,6 +968,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nfandroidtv/ @tkdrob
|
/tests/components/nfandroidtv/ @tkdrob
|
||||||
/homeassistant/components/nibe_heatpump/ @elupus
|
/homeassistant/components/nibe_heatpump/ @elupus
|
||||||
/tests/components/nibe_heatpump/ @elupus
|
/tests/components/nibe_heatpump/ @elupus
|
||||||
|
/homeassistant/components/nice_go/ @IceBotYT
|
||||||
|
/tests/components/nice_go/ @IceBotYT
|
||||||
/homeassistant/components/nightscout/ @marciogranzotto
|
/homeassistant/components/nightscout/ @marciogranzotto
|
||||||
/tests/components/nightscout/ @marciogranzotto
|
/tests/components/nightscout/ @marciogranzotto
|
||||||
/homeassistant/components/nilu/ @hfurubotten
|
/homeassistant/components/nilu/ @hfurubotten
|
||||||
@ -987,8 +1012,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/ollama/ @synesthesiam
|
/homeassistant/components/ollama/ @synesthesiam
|
||||||
/tests/components/ollama/ @synesthesiam
|
/tests/components/ollama/ @synesthesiam
|
||||||
/homeassistant/components/ombi/ @larssont
|
/homeassistant/components/ombi/ @larssont
|
||||||
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
|
|
||||||
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
|
|
||||||
/homeassistant/components/onboarding/ @home-assistant/core
|
/homeassistant/components/onboarding/ @home-assistant/core
|
||||||
/tests/components/onboarding/ @home-assistant/core
|
/tests/components/onboarding/ @home-assistant/core
|
||||||
/homeassistant/components/oncue/ @bdraco @peterager
|
/homeassistant/components/oncue/ @bdraco @peterager
|
||||||
@ -997,6 +1020,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ondilo_ico/ @JeromeHXP
|
/tests/components/ondilo_ico/ @JeromeHXP
|
||||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||||
/tests/components/onewire/ @garbled1 @epenet
|
/tests/components/onewire/ @garbled1 @epenet
|
||||||
|
/homeassistant/components/onkyo/ @arturpragacz
|
||||||
/homeassistant/components/onvif/ @hunterjm
|
/homeassistant/components/onvif/ @hunterjm
|
||||||
/tests/components/onvif/ @hunterjm
|
/tests/components/onvif/ @hunterjm
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
@ -1032,8 +1056,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/otbr/ @home-assistant/core
|
/tests/components/otbr/ @home-assistant/core
|
||||||
/homeassistant/components/ourgroceries/ @OnFreund
|
/homeassistant/components/ourgroceries/ @OnFreund
|
||||||
/tests/components/ourgroceries/ @OnFreund
|
/tests/components/ourgroceries/ @OnFreund
|
||||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
|
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
|
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||||
/homeassistant/components/ovo_energy/ @timmo001
|
/homeassistant/components/ovo_energy/ @timmo001
|
||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
@ -1185,8 +1209,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||||
/homeassistant/components/rmvtransport/ @cgtobi
|
/homeassistant/components/rmvtransport/ @cgtobi
|
||||||
/tests/components/rmvtransport/ @cgtobi
|
/tests/components/rmvtransport/ @cgtobi
|
||||||
/homeassistant/components/roborock/ @humbertogontijo @Lash-L
|
/homeassistant/components/roborock/ @Lash-L
|
||||||
/tests/components/roborock/ @humbertogontijo @Lash-L
|
/tests/components/roborock/ @Lash-L
|
||||||
/homeassistant/components/roku/ @ctalkington
|
/homeassistant/components/roku/ @ctalkington
|
||||||
/tests/components/roku/ @ctalkington
|
/tests/components/roku/ @ctalkington
|
||||||
/homeassistant/components/romy/ @xeniter
|
/homeassistant/components/romy/ @xeniter
|
||||||
@ -1203,6 +1227,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||||
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||||
|
/homeassistant/components/russound_rio/ @noahhusby
|
||||||
|
/tests/components/russound_rio/ @noahhusby
|
||||||
/homeassistant/components/ruuvi_gateway/ @akx
|
/homeassistant/components/ruuvi_gateway/ @akx
|
||||||
/tests/components/ruuvi_gateway/ @akx
|
/tests/components/ruuvi_gateway/ @akx
|
||||||
/homeassistant/components/ruuvitag_ble/ @akx
|
/homeassistant/components/ruuvitag_ble/ @akx
|
||||||
@ -1270,6 +1296,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sighthound/ @robmarkcole
|
/tests/components/sighthound/ @robmarkcole
|
||||||
/homeassistant/components/signal_messenger/ @bbernhard
|
/homeassistant/components/signal_messenger/ @bbernhard
|
||||||
/tests/components/signal_messenger/ @bbernhard
|
/tests/components/signal_messenger/ @bbernhard
|
||||||
|
/homeassistant/components/simplefin/ @scottg489 @jeeftor
|
||||||
|
/tests/components/simplefin/ @scottg489 @jeeftor
|
||||||
/homeassistant/components/simplepush/ @engrbm87
|
/homeassistant/components/simplepush/ @engrbm87
|
||||||
/tests/components/simplepush/ @engrbm87
|
/tests/components/simplepush/ @engrbm87
|
||||||
/homeassistant/components/simplisafe/ @bachya
|
/homeassistant/components/simplisafe/ @bachya
|
||||||
@ -1421,6 +1449,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
|
/tests/components/tesla_fleet/ @Bre77
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
/tests/components/tesla_wall_connector/ @einarhauks
|
/tests/components/tesla_wall_connector/ @einarhauks
|
||||||
/homeassistant/components/teslemetry/ @Bre77
|
/homeassistant/components/teslemetry/ @Bre77
|
||||||
@ -1459,8 +1489,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||||
/homeassistant/components/totalconnect/ @austinmroczek
|
/homeassistant/components/totalconnect/ @austinmroczek
|
||||||
/tests/components/totalconnect/ @austinmroczek
|
/tests/components/totalconnect/ @austinmroczek
|
||||||
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
|
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
|
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/homeassistant/components/tplink_omada/ @MarkGodwin
|
/homeassistant/components/tplink_omada/ @MarkGodwin
|
||||||
/tests/components/tplink_omada/ @MarkGodwin
|
/tests/components/tplink_omada/ @MarkGodwin
|
||||||
/homeassistant/components/traccar/ @ludeeus
|
/homeassistant/components/traccar/ @ludeeus
|
||||||
|
@ -12,7 +12,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.2.13
|
RUN pip3 install uv==0.2.27
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ coverage:
|
|||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: 90
|
target: auto
|
||||||
threshold: 0.09
|
threshold: 0.09
|
||||||
required:
|
required:
|
||||||
target: auto
|
target: auto
|
||||||
|
@ -28,6 +28,7 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA
|
|||||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||||
from .models import AuthFlowResult
|
from .models import AuthFlowResult
|
||||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||||
|
from .providers.homeassistant import HassAuthProvider
|
||||||
|
|
||||||
EVENT_USER_ADDED = "user_added"
|
EVENT_USER_ADDED = "user_added"
|
||||||
EVENT_USER_UPDATED = "user_updated"
|
EVENT_USER_UPDATED = "user_updated"
|
||||||
@ -73,6 +74,13 @@ async def auth_manager_from_config(
|
|||||||
key = (provider.type, provider.id)
|
key = (provider.type, provider.id)
|
||||||
provider_hash[key] = provider
|
provider_hash[key] = provider
|
||||||
|
|
||||||
|
if isinstance(provider, HassAuthProvider):
|
||||||
|
# Can be removed in 2026.7 with the legacy mode of homeassistant auth provider
|
||||||
|
# We need to initialize the provider to create the repair if needed as otherwise
|
||||||
|
# the provider will be initialized on first use, which could be rare as users
|
||||||
|
# don't frequently change auth settings
|
||||||
|
await provider.async_initialize()
|
||||||
|
|
||||||
if module_configs:
|
if module_configs:
|
||||||
modules = await asyncio.gather(
|
modules = await asyncio.gather(
|
||||||
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
|
||||||
@ -355,15 +363,15 @@ class AuthManager:
|
|||||||
local_only: bool | None = None,
|
local_only: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update a user."""
|
"""Update a user."""
|
||||||
kwargs: dict[str, Any] = {}
|
kwargs: dict[str, Any] = {
|
||||||
|
attr_name: value
|
||||||
for attr_name, value in (
|
for attr_name, value in (
|
||||||
("name", name),
|
("name", name),
|
||||||
("group_ids", group_ids),
|
("group_ids", group_ids),
|
||||||
("local_only", local_only),
|
("local_only", local_only),
|
||||||
):
|
)
|
||||||
if value is not None:
|
if value is not None
|
||||||
kwargs[attr_name] = value
|
}
|
||||||
await self._store.async_update_user(user, **kwargs)
|
await self._store.async_update_user(user, **kwargs)
|
||||||
|
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
@ -374,6 +382,13 @@ class AuthManager:
|
|||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id})
|
self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id})
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_user_credentials_data(
|
||||||
|
self, credentials: models.Credentials, data: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Update credentials data."""
|
||||||
|
self._store.async_update_user_credentials_data(credentials, data=data)
|
||||||
|
|
||||||
async def async_activate_user(self, user: models.User) -> None:
|
async def async_activate_user(self, user: models.User) -> None:
|
||||||
"""Activate a user."""
|
"""Activate a user."""
|
||||||
await self._store.async_activate_user(user)
|
await self._store.async_activate_user(user)
|
||||||
|
@ -105,14 +105,18 @@ class AuthStore:
|
|||||||
"perm_lookup": self._perm_lookup,
|
"perm_lookup": self._perm_lookup,
|
||||||
}
|
}
|
||||||
|
|
||||||
for attr_name, value in (
|
kwargs.update(
|
||||||
("is_owner", is_owner),
|
{
|
||||||
("is_active", is_active),
|
attr_name: value
|
||||||
("local_only", local_only),
|
for attr_name, value in (
|
||||||
("system_generated", system_generated),
|
("is_owner", is_owner),
|
||||||
):
|
("is_active", is_active),
|
||||||
if value is not None:
|
("local_only", local_only),
|
||||||
kwargs[attr_name] = value
|
("system_generated", system_generated),
|
||||||
|
)
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
@ -296,6 +300,14 @@ class AuthStore:
|
|||||||
refresh_token.expire_at = None
|
refresh_token.expire_at = None
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_user_credentials_data(
|
||||||
|
self, credentials: models.Credentials, data: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Update credentials data."""
|
||||||
|
credentials.data = data
|
||||||
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_load(self) -> None: # noqa: C901
|
async def async_load(self) -> None: # noqa: C901
|
||||||
"""Load the users."""
|
"""Load the users."""
|
||||||
if self._loaded:
|
if self._loaded:
|
||||||
|
@ -18,9 +18,12 @@ from homeassistant.const import (
|
|||||||
EVENT_THEMES_UPDATED,
|
EVENT_THEMES_UPDATED,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
||||||
|
from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED
|
||||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||||
|
from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED
|
||||||
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
||||||
|
from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED
|
||||||
from homeassistant.util.event_type import EventType
|
from homeassistant.util.event_type import EventType
|
||||||
|
|
||||||
# These are events that do not contain any sensitive data
|
# These are events that do not contain any sensitive data
|
||||||
@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
|||||||
EVENT_SHOPPING_LIST_UPDATED,
|
EVENT_SHOPPING_LIST_UPDATED,
|
||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
EVENT_THEMES_UPDATED,
|
EVENT_THEMES_UPDATED,
|
||||||
|
EVENT_LABEL_REGISTRY_UPDATED,
|
||||||
|
EVENT_CATEGORY_REGISTRY_UPDATED,
|
||||||
|
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import CONF_ID
|
from homeassistant.const import CONF_ID
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from ..models import AuthFlowResult, Credentials, UserMeta
|
from ..models import AuthFlowResult, Credentials, UserMeta
|
||||||
@ -54,6 +55,27 @@ class InvalidUser(HomeAssistantError):
|
|||||||
Will not be raised when validating authentication.
|
Will not be raised when validating authentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args: object,
|
||||||
|
translation_key: str | None = None,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize exception."""
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
translation_domain="auth",
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidUsername(InvalidUser):
|
||||||
|
"""Raised when invalid username is specified.
|
||||||
|
|
||||||
|
Will not be raised when validating authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
"""Hold the user data."""
|
"""Hold the user data."""
|
||||||
@ -67,13 +89,15 @@ class Data:
|
|||||||
self._data: dict[str, list[dict[str, str]]] | None = None
|
self._data: dict[str, list[dict[str, str]]] | None = None
|
||||||
# Legacy mode will allow usernames to start/end with whitespace
|
# Legacy mode will allow usernames to start/end with whitespace
|
||||||
# and will compare usernames case-insensitive.
|
# and will compare usernames case-insensitive.
|
||||||
# Remove in 2020 or when we launch 1.0.
|
# Deprecated in June 2019 and will be removed in 2026.7
|
||||||
self.is_legacy = False
|
self.is_legacy = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def normalize_username(self, username: str) -> str:
|
def normalize_username(
|
||||||
|
self, username: str, *, force_normalize: bool = False
|
||||||
|
) -> str:
|
||||||
"""Normalize a username based on the mode."""
|
"""Normalize a username based on the mode."""
|
||||||
if self.is_legacy:
|
if self.is_legacy and not force_normalize:
|
||||||
return username
|
return username
|
||||||
|
|
||||||
return username.strip().casefold()
|
return username.strip().casefold()
|
||||||
@ -83,44 +107,49 @@ class Data:
|
|||||||
if (data := await self._store.async_load()) is None:
|
if (data := await self._store.async_load()) is None:
|
||||||
data = cast(dict[str, list[dict[str, str]]], {"users": []})
|
data = cast(dict[str, list[dict[str, str]]], {"users": []})
|
||||||
|
|
||||||
seen: set[str] = set()
|
self._async_check_for_not_normalized_usernames(data)
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_check_for_not_normalized_usernames(
|
||||||
|
self, data: dict[str, list[dict[str, str]]]
|
||||||
|
) -> None:
|
||||||
|
not_normalized_usernames: set[str] = set()
|
||||||
|
|
||||||
for user in data["users"]:
|
for user in data["users"]:
|
||||||
username = user["username"]
|
username = user["username"]
|
||||||
|
|
||||||
# check if we have duplicates
|
if self.normalize_username(username, force_normalize=True) != username:
|
||||||
if (folded := username.casefold()) in seen:
|
|
||||||
self.is_legacy = True
|
|
||||||
|
|
||||||
logging.getLogger(__name__).warning(
|
logging.getLogger(__name__).warning(
|
||||||
(
|
(
|
||||||
"Home Assistant auth provider is running in legacy mode "
|
"Home Assistant auth provider is running in legacy mode "
|
||||||
"because we detected usernames that are case-insensitive"
|
"because we detected usernames that are normalized (lowercase and without spaces)."
|
||||||
"equivalent. Please change the username: '%s'."
|
" Please change the username: '%s'."
|
||||||
),
|
),
|
||||||
username,
|
username,
|
||||||
)
|
)
|
||||||
|
not_normalized_usernames.add(username)
|
||||||
|
|
||||||
break
|
if not_normalized_usernames:
|
||||||
|
self.is_legacy = True
|
||||||
seen.add(folded)
|
ir.async_create_issue(
|
||||||
|
self.hass,
|
||||||
# check if we have unstripped usernames
|
"auth",
|
||||||
if username != username.strip():
|
"homeassistant_provider_not_normalized_usernames",
|
||||||
self.is_legacy = True
|
breaks_in_ha_version="2026.7.0",
|
||||||
|
is_fixable=False,
|
||||||
logging.getLogger(__name__).warning(
|
severity=ir.IssueSeverity.WARNING,
|
||||||
(
|
translation_key="homeassistant_provider_not_normalized_usernames",
|
||||||
"Home Assistant auth provider is running in legacy mode "
|
translation_placeholders={
|
||||||
"because we detected usernames that start or end in a "
|
"usernames": f'- "{'"\n- "'.join(sorted(not_normalized_usernames))}"'
|
||||||
"space. Please change the username: '%s'."
|
},
|
||||||
),
|
learn_more_url="homeassistant://config/users",
|
||||||
username,
|
)
|
||||||
)
|
else:
|
||||||
|
self.is_legacy = False
|
||||||
break
|
ir.async_delete_issue(
|
||||||
|
self.hass, "auth", "homeassistant_provider_not_normalized_usernames"
|
||||||
self._data = data
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self) -> list[dict[str, str]]:
|
def users(self) -> list[dict[str, str]]:
|
||||||
@ -162,13 +191,11 @@ class Data:
|
|||||||
return hashed
|
return hashed
|
||||||
|
|
||||||
def add_auth(self, username: str, password: str) -> None:
|
def add_auth(self, username: str, password: str) -> None:
|
||||||
"""Add a new authenticated user/pass."""
|
"""Add a new authenticated user/pass.
|
||||||
username = self.normalize_username(username)
|
|
||||||
|
|
||||||
if any(
|
Raises InvalidUsername if the new username is invalid.
|
||||||
self.normalize_username(user["username"]) == username for user in self.users
|
"""
|
||||||
):
|
self._validate_new_username(username)
|
||||||
raise InvalidUser
|
|
||||||
|
|
||||||
self.users.append(
|
self.users.append(
|
||||||
{
|
{
|
||||||
@ -189,7 +216,7 @@ class Data:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if index is None:
|
if index is None:
|
||||||
raise InvalidUser
|
raise InvalidUser(translation_key="user_not_found")
|
||||||
|
|
||||||
self.users.pop(index)
|
self.users.pop(index)
|
||||||
|
|
||||||
@ -205,7 +232,50 @@ class Data:
|
|||||||
user["password"] = self.hash_password(new_password, True).decode()
|
user["password"] = self.hash_password(new_password, True).decode()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidUser
|
raise InvalidUser(translation_key="user_not_found")
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _validate_new_username(self, new_username: str) -> None:
|
||||||
|
"""Validate that username is normalized and unique.
|
||||||
|
|
||||||
|
Raises InvalidUsername if the new username is invalid.
|
||||||
|
"""
|
||||||
|
normalized_username = self.normalize_username(
|
||||||
|
new_username, force_normalize=True
|
||||||
|
)
|
||||||
|
if normalized_username != new_username:
|
||||||
|
raise InvalidUsername(
|
||||||
|
translation_key="username_not_normalized",
|
||||||
|
translation_placeholders={"new_username": new_username},
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(
|
||||||
|
self.normalize_username(user["username"]) == normalized_username
|
||||||
|
for user in self.users
|
||||||
|
):
|
||||||
|
raise InvalidUsername(
|
||||||
|
translation_key="username_already_exists",
|
||||||
|
translation_placeholders={"username": new_username},
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def change_username(self, username: str, new_username: str) -> None:
|
||||||
|
"""Update the username.
|
||||||
|
|
||||||
|
Raises InvalidUser if user cannot be found.
|
||||||
|
Raises InvalidUsername if the new username is invalid.
|
||||||
|
"""
|
||||||
|
username = self.normalize_username(username)
|
||||||
|
self._validate_new_username(new_username)
|
||||||
|
|
||||||
|
for user in self.users:
|
||||||
|
if self.normalize_username(user["username"]) == username:
|
||||||
|
user["username"] = new_username
|
||||||
|
assert self._data is not None
|
||||||
|
self._async_check_for_not_normalized_usernames(self._data)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise InvalidUser(translation_key="user_not_found")
|
||||||
|
|
||||||
async def async_save(self) -> None:
|
async def async_save(self) -> None:
|
||||||
"""Save data."""
|
"""Save data."""
|
||||||
@ -278,6 +348,20 @@ class HassAuthProvider(AuthProvider):
|
|||||||
)
|
)
|
||||||
await self.data.async_save()
|
await self.data.async_save()
|
||||||
|
|
||||||
|
async def async_change_username(
|
||||||
|
self, credential: Credentials, new_username: str
|
||||||
|
) -> None:
|
||||||
|
"""Validate new username and change it including updating credentials object."""
|
||||||
|
if self.data is None:
|
||||||
|
await self.async_initialize()
|
||||||
|
assert self.data is not None
|
||||||
|
|
||||||
|
self.data.change_username(credential.data["username"], new_username)
|
||||||
|
self.hass.auth.async_update_user_credentials_data(
|
||||||
|
credential, {**credential.data, "username": new_username}
|
||||||
|
)
|
||||||
|
await self.data.async_save()
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Mapping[str, str]
|
self, flow_result: Mapping[str, str]
|
||||||
) -> Credentials:
|
) -> Credentials:
|
||||||
|
@ -8,6 +8,8 @@ import glob
|
|||||||
from http.client import HTTPConnection
|
from http.client import HTTPConnection
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from ssl import SSLContext
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -143,6 +145,78 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
|
|||||||
strict_core=False,
|
strict_core=False,
|
||||||
skip_for_tests=True,
|
skip_for_tests=True,
|
||||||
),
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=SSLContext.load_default_certs,
|
||||||
|
object=SSLContext,
|
||||||
|
function="load_default_certs",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=SSLContext.load_verify_locations,
|
||||||
|
object=SSLContext,
|
||||||
|
function="load_verify_locations",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=SSLContext.load_cert_chain,
|
||||||
|
object=SSLContext,
|
||||||
|
function="load_cert_chain",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=Path.open,
|
||||||
|
object=Path,
|
||||||
|
function="open",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=Path.read_text,
|
||||||
|
object=Path,
|
||||||
|
function="read_text",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=Path.read_bytes,
|
||||||
|
object=Path,
|
||||||
|
function="read_bytes",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=Path.write_text,
|
||||||
|
object=Path,
|
||||||
|
function="write_text",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=Path.write_bytes,
|
||||||
|
object=Path,
|
||||||
|
function="write_bytes",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import contextlib
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from operator import contains, itemgetter
|
from operator import contains, itemgetter
|
||||||
import os
|
import os
|
||||||
@ -88,7 +88,7 @@ from .helpers import (
|
|||||||
)
|
)
|
||||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||||
from .helpers.storage import get_internal_store_manager
|
from .helpers.storage import get_internal_store_manager
|
||||||
from .helpers.system_info import async_get_system_info
|
from .helpers.system_info import async_get_system_info, is_official_image
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .setup import (
|
from .setup import (
|
||||||
# _setup_started is marked as protected to make it clear
|
# _setup_started is marked as protected to make it clear
|
||||||
@ -104,7 +104,7 @@ from .setup import (
|
|||||||
from .util.async_ import create_eager_task
|
from .util.async_ import create_eager_task
|
||||||
from .util.hass_dict import HassKey
|
from .util.hass_dict import HassKey
|
||||||
from .util.logging import async_activate_log_queue_handler
|
from .util.logging import async_activate_log_queue_handler
|
||||||
from .util.package import async_get_user_site, is_virtual_env
|
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||||
|
|
||||||
with contextlib.suppress(ImportError):
|
with contextlib.suppress(ImportError):
|
||||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||||
@ -223,8 +223,10 @@ CRITICAL_INTEGRATIONS = {
|
|||||||
SETUP_ORDER = (
|
SETUP_ORDER = (
|
||||||
# Load logging and http deps as soon as possible
|
# Load logging and http deps as soon as possible
|
||||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||||
# Setup frontend and recorder
|
# Setup frontend
|
||||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
("frontend", FRONTEND_INTEGRATIONS),
|
||||||
|
# Setup recorder
|
||||||
|
("recorder", RECORDER_INTEGRATIONS),
|
||||||
# Start up debuggers. Start these first in case they want to wait.
|
# Start up debuggers. Start these first in case they want to wait.
|
||||||
("debugger", DEBUGGER_INTEGRATIONS),
|
("debugger", DEBUGGER_INTEGRATIONS),
|
||||||
)
|
)
|
||||||
@ -257,12 +259,12 @@ async def async_setup_hass(
|
|||||||
) -> core.HomeAssistant | None:
|
) -> core.HomeAssistant | None:
|
||||||
"""Set up Home Assistant."""
|
"""Set up Home Assistant."""
|
||||||
|
|
||||||
def create_hass() -> core.HomeAssistant:
|
async def create_hass() -> core.HomeAssistant:
|
||||||
"""Create the hass object and do basic setup."""
|
"""Create the hass object and do basic setup."""
|
||||||
hass = core.HomeAssistant(runtime_config.config_dir)
|
hass = core.HomeAssistant(runtime_config.config_dir)
|
||||||
loader.async_setup(hass)
|
loader.async_setup(hass)
|
||||||
|
|
||||||
async_enable_logging(
|
await async_enable_logging(
|
||||||
hass,
|
hass,
|
||||||
runtime_config.verbose,
|
runtime_config.verbose,
|
||||||
runtime_config.log_rotate_days,
|
runtime_config.log_rotate_days,
|
||||||
@ -287,7 +289,7 @@ async def async_setup_hass(
|
|||||||
async with hass.timeout.async_timeout(10):
|
async with hass.timeout.async_timeout(10):
|
||||||
await hass.async_stop()
|
await hass.async_stop()
|
||||||
|
|
||||||
hass = create_hass()
|
hass = await create_hass()
|
||||||
|
|
||||||
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@ -326,13 +328,13 @@ async def async_setup_hass(
|
|||||||
if config_dict is None:
|
if config_dict is None:
|
||||||
recovery_mode = True
|
recovery_mode = True
|
||||||
await stop_hass(hass)
|
await stop_hass(hass)
|
||||||
hass = create_hass()
|
hass = await create_hass()
|
||||||
|
|
||||||
elif not basic_setup_success:
|
elif not basic_setup_success:
|
||||||
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
||||||
recovery_mode = True
|
recovery_mode = True
|
||||||
await stop_hass(hass)
|
await stop_hass(hass)
|
||||||
hass = create_hass()
|
hass = await create_hass()
|
||||||
|
|
||||||
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
@ -345,7 +347,7 @@ async def async_setup_hass(
|
|||||||
|
|
||||||
recovery_mode = True
|
recovery_mode = True
|
||||||
await stop_hass(hass)
|
await stop_hass(hass)
|
||||||
hass = create_hass()
|
hass = await create_hass()
|
||||||
|
|
||||||
if old_logging:
|
if old_logging:
|
||||||
hass.data[DATA_LOGGING] = old_logging
|
hass.data[DATA_LOGGING] = old_logging
|
||||||
@ -407,6 +409,10 @@ def _init_blocking_io_modules_in_executor() -> None:
|
|||||||
# Initialize the mimetypes module to avoid blocking calls
|
# Initialize the mimetypes module to avoid blocking calls
|
||||||
# to the filesystem to load the mime.types file.
|
# to the filesystem to load the mime.types file.
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
|
# Initialize is_official_image and is_docker_env to avoid blocking calls
|
||||||
|
# to the filesystem.
|
||||||
|
is_official_image()
|
||||||
|
is_docker_env()
|
||||||
|
|
||||||
|
|
||||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||||
@ -523,8 +529,7 @@ async def async_from_config_dict(
|
|||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
@core.callback
|
async def async_enable_logging(
|
||||||
def async_enable_logging(
|
|
||||||
hass: core.HomeAssistant,
|
hass: core.HomeAssistant,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
log_rotate_days: int | None = None,
|
log_rotate_days: int | None = None,
|
||||||
@ -581,10 +586,10 @@ def async_enable_logging(
|
|||||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
|
||||||
sys.excepthook = lambda *args: logging.getLogger(None).exception(
|
sys.excepthook = lambda *args: logging.getLogger().exception(
|
||||||
"Uncaught exception", exc_info=args
|
"Uncaught exception", exc_info=args
|
||||||
)
|
)
|
||||||
threading.excepthook = lambda args: logging.getLogger(None).exception(
|
threading.excepthook = lambda args: logging.getLogger().exception(
|
||||||
"Uncaught thread exception",
|
"Uncaught thread exception",
|
||||||
exc_info=( # type: ignore[arg-type]
|
exc_info=( # type: ignore[arg-type]
|
||||||
args.exc_type,
|
args.exc_type,
|
||||||
@ -607,28 +612,13 @@ def async_enable_logging(
|
|||||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||||
):
|
):
|
||||||
err_handler: (
|
err_handler = await hass.async_add_executor_job(
|
||||||
logging.handlers.RotatingFileHandler
|
_create_log_file, err_log_path, log_rotate_days
|
||||||
| logging.handlers.TimedRotatingFileHandler
|
|
||||||
)
|
)
|
||||||
if log_rotate_days:
|
|
||||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
|
||||||
err_log_path, when="midnight", backupCount=log_rotate_days
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
|
||||||
err_log_path, backupCount=1
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
err_handler.doRollover()
|
|
||||||
except OSError as err:
|
|
||||||
_LOGGER.error("Error rolling over log file: %s", err)
|
|
||||||
|
|
||||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
|
||||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||||
|
|
||||||
logger = logging.getLogger("")
|
logger = logging.getLogger()
|
||||||
logger.addHandler(err_handler)
|
logger.addHandler(err_handler)
|
||||||
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||||
|
|
||||||
@ -640,7 +630,29 @@ def async_enable_logging(
|
|||||||
async_activate_log_queue_handler(hass)
|
async_activate_log_queue_handler(hass)
|
||||||
|
|
||||||
|
|
||||||
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler):
|
def _create_log_file(
|
||||||
|
err_log_path: str, log_rotate_days: int | None
|
||||||
|
) -> RotatingFileHandler | TimedRotatingFileHandler:
|
||||||
|
"""Create log file and do roll over."""
|
||||||
|
err_handler: RotatingFileHandler | TimedRotatingFileHandler
|
||||||
|
if log_rotate_days:
|
||||||
|
err_handler = TimedRotatingFileHandler(
|
||||||
|
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
||||||
|
err_log_path, backupCount=1
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
err_handler.doRollover()
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Error rolling over log file: %s", err)
|
||||||
|
|
||||||
|
return err_handler
|
||||||
|
|
||||||
|
|
||||||
|
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
|
||||||
"""RotatingFileHandler that does not check if it should roll over on every log."""
|
"""RotatingFileHandler that does not check if it should roll over on every log."""
|
||||||
|
|
||||||
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
||||||
@ -895,7 +907,13 @@ async def _async_resolve_domains_to_setup(
|
|||||||
await asyncio.gather(*resolve_dependencies_tasks)
|
await asyncio.gather(*resolve_dependencies_tasks)
|
||||||
|
|
||||||
for itg in integrations_to_process:
|
for itg in integrations_to_process:
|
||||||
for dep in itg.all_dependencies:
|
try:
|
||||||
|
all_deps = itg.all_dependencies
|
||||||
|
except RuntimeError:
|
||||||
|
# Integration.all_dependencies raises RuntimeError if
|
||||||
|
# dependencies could not be resolved
|
||||||
|
continue
|
||||||
|
for dep in all_deps:
|
||||||
if dep in domains_to_setup:
|
if dep in domains_to_setup:
|
||||||
continue
|
continue
|
||||||
domains_to_setup.add(dep)
|
domains_to_setup.add(dep)
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "asterisk",
|
|
||||||
"name": "Asterisk",
|
|
||||||
"integrations": ["asterisk_cdr", "asterisk_mbox"]
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "logitech",
|
"domain": "logitech",
|
||||||
"name": "Logitech",
|
"name": "Logitech",
|
||||||
"integrations": ["harmony", "ue_smart_radio", "squeezebox"]
|
"integrations": ["harmony", "squeezebox"]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "tesla",
|
"domain": "tesla",
|
||||||
"name": "Tesla",
|
"name": "Tesla",
|
||||||
"integrations": ["powerwall", "tesla_wall_connector"]
|
"integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"]
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,6 @@ from __future__ import annotations
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from jaraco.abode.devices.sensor import BinarySensor
|
from jaraco.abode.devices.sensor import BinarySensor
|
||||||
from jaraco.abode.helpers.constants import (
|
|
||||||
TYPE_CONNECTIVITY,
|
|
||||||
TYPE_MOISTURE,
|
|
||||||
TYPE_MOTION,
|
|
||||||
TYPE_OCCUPANCY,
|
|
||||||
TYPE_OPENING,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
@ -34,11 +27,11 @@ async def async_setup_entry(
|
|||||||
data: AbodeSystem = hass.data[DOMAIN]
|
data: AbodeSystem = hass.data[DOMAIN]
|
||||||
|
|
||||||
device_types = [
|
device_types = [
|
||||||
TYPE_CONNECTIVITY,
|
"connectivity",
|
||||||
TYPE_MOISTURE,
|
"moisture",
|
||||||
TYPE_MOTION,
|
"motion",
|
||||||
TYPE_OCCUPANCY,
|
"occupancy",
|
||||||
TYPE_OPENING,
|
"door",
|
||||||
]
|
]
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
|
@ -8,7 +8,6 @@ from typing import Any, cast
|
|||||||
from jaraco.abode.devices.base import Device
|
from jaraco.abode.devices.base import Device
|
||||||
from jaraco.abode.devices.camera import Camera as AbodeCam
|
from jaraco.abode.devices.camera import Camera as AbodeCam
|
||||||
from jaraco.abode.helpers import timeline
|
from jaraco.abode.helpers import timeline
|
||||||
from jaraco.abode.helpers.constants import TYPE_CAMERA
|
|
||||||
import requests
|
import requests
|
||||||
from requests.models import Response
|
from requests.models import Response
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
|
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
|
||||||
for device in data.abode.get_devices(generic_type=TYPE_CAMERA)
|
for device in data.abode.get_devices(generic_type="camera")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from jaraco.abode.devices.cover import Cover
|
from jaraco.abode.devices.cover import Cover
|
||||||
from jaraco.abode.helpers.constants import TYPE_COVER
|
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverEntity
|
from homeassistant.components.cover import CoverEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -23,7 +22,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AbodeCover(data, device)
|
AbodeCover(data, device)
|
||||||
for device in data.abode.get_devices(generic_type=TYPE_COVER)
|
for device in data.abode.get_devices(generic_type="cover")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class AbodeAutomation(AbodeEntity):
|
|||||||
super().__init__(data)
|
super().__init__(data)
|
||||||
self._automation = automation
|
self._automation = automation
|
||||||
self._attr_name = automation.name
|
self._attr_name = automation.name
|
||||||
self._attr_unique_id = automation.automation_id
|
self._attr_unique_id = automation.id
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
"type": "CUE automation",
|
"type": "CUE automation",
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ from math import ceil
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from jaraco.abode.devices.light import Light
|
from jaraco.abode.devices.light import Light
|
||||||
from jaraco.abode.helpers.constants import TYPE_LIGHT
|
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
@ -36,7 +35,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AbodeLight(data, device)
|
AbodeLight(data, device)
|
||||||
for device in data.abode.get_devices(generic_type=TYPE_LIGHT)
|
for device in data.abode.get_devices(generic_type="light")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from jaraco.abode.devices.lock import Lock
|
from jaraco.abode.devices.lock import Lock
|
||||||
from jaraco.abode.helpers.constants import TYPE_LOCK
|
|
||||||
|
|
||||||
from homeassistant.components.lock import LockEntity
|
from homeassistant.components.lock import LockEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -23,7 +22,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AbodeLock(data, device)
|
AbodeLock(data, device)
|
||||||
for device in data.abode.get_devices(generic_type=TYPE_LOCK)
|
for device in data.abode.get_devices(generic_type="lock")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["jaraco.abode", "lomond"],
|
"loggers": ["jaraco.abode", "lomond"],
|
||||||
"requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"]
|
"requirements": ["jaraco.abode==5.2.1"]
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,6 @@ from dataclasses import dataclass
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from jaraco.abode.devices.sensor import Sensor
|
from jaraco.abode.devices.sensor import Sensor
|
||||||
from jaraco.abode.helpers.constants import (
|
|
||||||
HUMI_STATUS_KEY,
|
|
||||||
LUX_STATUS_KEY,
|
|
||||||
STATUSES_KEY,
|
|
||||||
TEMP_STATUS_KEY,
|
|
||||||
TYPE_SENSOR,
|
|
||||||
UNIT_CELSIUS,
|
|
||||||
UNIT_FAHRENHEIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -32,8 +23,8 @@ from .const import DOMAIN
|
|||||||
from .entity import AbodeDevice
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||||
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
"°F": UnitOfTemperature.FAHRENHEIT,
|
||||||
UNIT_CELSIUS: UnitOfTemperature.CELSIUS,
|
"°C": UnitOfTemperature.CELSIUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +38,7 @@ class AbodeSensorDescription(SensorEntityDescription):
|
|||||||
|
|
||||||
SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key=TEMP_STATUS_KEY,
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||||
device.temp_unit
|
device.temp_unit
|
||||||
@ -55,13 +46,13 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
|||||||
value_fn=lambda device: cast(float, device.temp),
|
value_fn=lambda device: cast(float, device.temp),
|
||||||
),
|
),
|
||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key=HUMI_STATUS_KEY,
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||||
value_fn=lambda device: cast(float, device.humidity),
|
value_fn=lambda device: cast(float, device.humidity),
|
||||||
),
|
),
|
||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key=LUX_STATUS_KEY,
|
key="lux",
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||||
value_fn=lambda device: cast(float, device.lux),
|
value_fn=lambda device: cast(float, device.lux),
|
||||||
@ -78,8 +69,8 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
AbodeSensor(data, device, description)
|
AbodeSensor(data, device, description)
|
||||||
for description in SENSOR_TYPES
|
for description in SENSOR_TYPES
|
||||||
for device in data.abode.get_devices(generic_type=TYPE_SENSOR)
|
for device in data.abode.get_devices(generic_type="sensor")
|
||||||
if description.key in device.get_value(STATUSES_KEY)
|
if description.key in device.get_value("statuses")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from jaraco.abode.devices.switch import Switch
|
from jaraco.abode.devices.switch import Switch
|
||||||
from jaraco.abode.helpers.constants import TYPE_SWITCH, TYPE_VALVE
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -17,7 +16,7 @@ from . import AbodeSystem
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import AbodeAutomation, AbodeDevice
|
from .entity import AbodeAutomation, AbodeDevice
|
||||||
|
|
||||||
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
|
DEVICE_TYPES = ["switch", "valve"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -89,4 +88,4 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if the automation is enabled."""
|
"""Return True if the automation is enabled."""
|
||||||
return bool(self._automation.is_enabled)
|
return bool(self._automation.enabled)
|
||||||
|
@ -9,7 +9,10 @@ from typing import Any
|
|||||||
import serial
|
import serial
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
from homeassistant.components.switch import (
|
||||||
|
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
|
||||||
|
SwitchEntity,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_FILENAME,
|
CONF_FILENAME,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -38,7 +41,7 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_FILENAME): cv.isdevice,
|
vol.Required(CONF_FILENAME): cv.isdevice,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
@ -78,7 +81,7 @@ class AcerSwitch(SwitchEntity):
|
|||||||
write_timeout: int,
|
write_timeout: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init of the Acer projector."""
|
"""Init of the Acer projector."""
|
||||||
self.ser = serial.Serial(
|
self.serial = serial.Serial(
|
||||||
port=serial_port, timeout=timeout, write_timeout=write_timeout
|
port=serial_port, timeout=timeout, write_timeout=write_timeout
|
||||||
)
|
)
|
||||||
self._serial_port = serial_port
|
self._serial_port = serial_port
|
||||||
@ -96,16 +99,16 @@ class AcerSwitch(SwitchEntity):
|
|||||||
# was disconnected during runtime.
|
# was disconnected during runtime.
|
||||||
# This way the projector can be reconnected and will still work
|
# This way the projector can be reconnected and will still work
|
||||||
try:
|
try:
|
||||||
if not self.ser.is_open:
|
if not self.serial.is_open:
|
||||||
self.ser.open()
|
self.serial.open()
|
||||||
self.ser.write(msg.encode("utf-8"))
|
self.serial.write(msg.encode("utf-8"))
|
||||||
# Size is an experience value there is no real limit.
|
# Size is an experience value there is no real limit.
|
||||||
# AFAIK there is no limit and no end character so we will usually
|
# AFAIK there is no limit and no end character so we will usually
|
||||||
# need to wait for timeout
|
# need to wait for timeout
|
||||||
ret = self.ser.read_until(size=20).decode("utf-8")
|
ret = self.serial.read_until(size=20).decode("utf-8")
|
||||||
except serial.SerialException:
|
except serial.SerialException:
|
||||||
_LOGGER.error("Problem communicating with %s", self._serial_port)
|
_LOGGER.error("Problem communicating with %s", self._serial_port)
|
||||||
self.ser.close()
|
self.serial.close()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _write_read_format(self, msg: str) -> str:
|
def _write_read_format(self, msg: str) -> str:
|
||||||
|
@ -10,7 +10,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
|
||||||
DeviceScanner,
|
DeviceScanner,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
@ -23,7 +23,7 @@ from .model import Device
|
|||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adguardhome"],
|
"loggers": ["adguardhome"],
|
||||||
"requirements": ["adguardhome==0.6.3"]
|
"requirements": ["adguardhome==0.7.0"]
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
# Tuple to hold data needed for notification
|
# Tuple to hold data needed for notification
|
||||||
NotificationItem = namedtuple(
|
NotificationItem = namedtuple( # noqa: PYI024
|
||||||
"NotificationItem", "hnotify huser name plc_datatype callback"
|
"NotificationItem", "hnotify huser name plc_datatype callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA,
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||||||
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS binary sensor"
|
DEFAULT_NAME = "ADS binary sensor"
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
@ -10,7 +10,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
|
||||||
CoverEntity,
|
CoverEntity,
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
@ -36,7 +36,7 @@ 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"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
vol.Optional(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||||
|
@ -9,7 +9,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
@ -29,7 +29,7 @@ from . import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS Light"
|
DEFAULT_NAME = "ADS Light"
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
|
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
|
||||||
|
@ -4,7 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||||
|
SensorEntity,
|
||||||
|
)
|
||||||
from homeassistant.const import 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
|
||||||
@ -22,7 +25,7 @@ from . import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_NAME = "ADS sensor"
|
DEFAULT_NAME = "ADS sensor"
|
||||||
PLATFORM_SCHEMA = 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,
|
||||||
|
@ -7,7 +7,10 @@ from typing import Any
|
|||||||
import pyads
|
import pyads
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
from homeassistant.components.switch import (
|
||||||
|
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
|
||||||
|
SwitchEntity,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -18,7 +21,7 @@ from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
|||||||
|
|
||||||
DEFAULT_NAME = "ADS Switch"
|
DEFAULT_NAME = "ADS Switch"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
@ -206,7 +206,8 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
|||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set the HVAC Mode and State."""
|
"""Set the HVAC Mode and State."""
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode == HVACMode.OFF:
|
||||||
return await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
return
|
||||||
if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
|
if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
|
||||||
raise ServiceValidationError("Heat/Cool is not supported in this mode")
|
raise ServiceValidationError("Heat/Cool is not supported in this mode")
|
||||||
await self.async_update_ac(
|
await self.async_update_ac(
|
||||||
|
@ -7,14 +7,35 @@ from typing import Any
|
|||||||
from aemet_opendata.helpers import dict_nested_value
|
from aemet_opendata.helpers import dict_nested_value
|
||||||
|
|
||||||
from homeassistant.components.weather import Forecast
|
from homeassistant.components.weather import Forecast
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]):
|
class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]):
|
||||||
"""Define an AEMET entity."""
|
"""Define an AEMET entity."""
|
||||||
|
|
||||||
|
_attr_attribution = ATTRIBUTION
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: WeatherUpdateCoordinator,
|
||||||
|
name: str,
|
||||||
|
unique_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
name=name,
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
identifiers={(DOMAIN, unique_id)},
|
||||||
|
manufacturer="AEMET",
|
||||||
|
model="Forecast",
|
||||||
|
)
|
||||||
|
|
||||||
def get_aemet_forecast(self, forecast_mode: str) -> list[Forecast]:
|
def get_aemet_forecast(self, forecast_mode: str) -> list[Forecast]:
|
||||||
"""Return AEMET entity forecast by mode."""
|
"""Return AEMET entity forecast by mode."""
|
||||||
return self.coordinator.data["forecast"][forecast_mode]
|
return self.coordinator.data["forecast"][forecast_mode]
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.5.2"]
|
"requirements": ["AEMET-OpenData==0.5.4"]
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
DEGREE,
|
DEGREE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
@ -86,7 +85,6 @@ from .const import (
|
|||||||
ATTR_API_WIND_BEARING,
|
ATTR_API_WIND_BEARING,
|
||||||
ATTR_API_WIND_MAX_SPEED,
|
ATTR_API_WIND_MAX_SPEED,
|
||||||
ATTR_API_WIND_SPEED,
|
ATTR_API_WIND_SPEED,
|
||||||
ATTRIBUTION,
|
|
||||||
CONDITIONS_MAP,
|
CONDITIONS_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
@ -366,12 +364,15 @@ async def async_setup_entry(
|
|||||||
name = domain_data.name
|
name = domain_data.name
|
||||||
coordinator = domain_data.coordinator
|
coordinator = domain_data.coordinator
|
||||||
|
|
||||||
|
unique_id = config_entry.unique_id
|
||||||
|
assert unique_id is not None
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AemetSensor(
|
AemetSensor(
|
||||||
name,
|
name,
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
config_entry,
|
unique_id,
|
||||||
)
|
)
|
||||||
for description in FORECAST_SENSORS + WEATHER_SENSORS
|
for description in FORECAST_SENSORS + WEATHER_SENSORS
|
||||||
if dict_nested_value(coordinator.data["lib"], description.keys) is not None
|
if dict_nested_value(coordinator.data["lib"], description.keys) is not None
|
||||||
@ -381,7 +382,6 @@ async def async_setup_entry(
|
|||||||
class AemetSensor(AemetEntity, SensorEntity):
|
class AemetSensor(AemetEntity, SensorEntity):
|
||||||
"""Implementation of an AEMET OpenData sensor."""
|
"""Implementation of an AEMET OpenData sensor."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
|
||||||
entity_description: AemetSensorEntityDescription
|
entity_description: AemetSensorEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -389,13 +389,12 @@ class AemetSensor(AemetEntity, SensorEntity):
|
|||||||
name: str,
|
name: str,
|
||||||
coordinator: WeatherUpdateCoordinator,
|
coordinator: WeatherUpdateCoordinator,
|
||||||
description: AemetSensorEntityDescription,
|
description: AemetSensorEntityDescription,
|
||||||
config_entry: ConfigEntry,
|
unique_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, name, unique_id)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_name = f"{name} {description.name}"
|
self._attr_unique_id = f"{unique_id}-{description.key}"
|
||||||
self._attr_unique_id = f"{config_entry.unique_id}-{description.key}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self):
|
||||||
|
@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AemetConfigEntry
|
from . import AemetConfigEntry
|
||||||
from .const import ATTRIBUTION, CONDITIONS_MAP
|
from .const import CONDITIONS_MAP
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
from .entity import AemetEntity
|
from .entity import AemetEntity
|
||||||
|
|
||||||
@ -43,10 +43,10 @@ async def async_setup_entry(
|
|||||||
name = domain_data.name
|
name = domain_data.name
|
||||||
weather_coordinator = domain_data.coordinator
|
weather_coordinator = domain_data.coordinator
|
||||||
|
|
||||||
async_add_entities(
|
unique_id = config_entry.unique_id
|
||||||
[AemetWeather(name, config_entry.unique_id, weather_coordinator)],
|
assert unique_id is not None
|
||||||
False,
|
|
||||||
)
|
async_add_entities([AemetWeather(name, unique_id, weather_coordinator)])
|
||||||
|
|
||||||
|
|
||||||
class AemetWeather(
|
class AemetWeather(
|
||||||
@ -55,7 +55,6 @@ class AemetWeather(
|
|||||||
):
|
):
|
||||||
"""Implementation of an AEMET OpenData weather."""
|
"""Implementation of an AEMET OpenData weather."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
|
||||||
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
@ -63,16 +62,16 @@ class AemetWeather(
|
|||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||||
)
|
)
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: str,
|
||||||
unique_id,
|
unique_id: str,
|
||||||
coordinator: WeatherUpdateCoordinator,
|
coordinator: WeatherUpdateCoordinator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, name, unique_id)
|
||||||
self._attr_name = name
|
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -59,7 +59,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
platform = async_get_current_platform()
|
platform = async_get_current_platform()
|
||||||
for service, method in CAMERA_SERVICES.items():
|
for service, method in CAMERA_SERVICES.items():
|
||||||
platform.async_register_entity_service(service, {}, method)
|
platform.async_register_entity_service(service, None, method)
|
||||||
|
|
||||||
|
|
||||||
class AgentCamera(MjpegCamera):
|
class AgentCamera(MjpegCamera):
|
||||||
|
@ -9,10 +9,7 @@ from typing import Final, final
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
from homeassistant.helpers import config_validation as cv
|
||||||
PLATFORM_SCHEMA,
|
|
||||||
PLATFORM_SCHEMA_BASE,
|
|
||||||
)
|
|
||||||
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
|
||||||
@ -21,6 +18,11 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||||
|
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||||
|
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||||
|
|
||||||
ATTR_AQI: Final = "air_quality_index"
|
ATTR_AQI: Final = "air_quality_index"
|
||||||
ATTR_CO2: Final = "carbon_dioxide"
|
ATTR_CO2: Final = "carbon_dioxide"
|
||||||
ATTR_CO: Final = "carbon_monoxide"
|
ATTR_CO: Final = "carbon_monoxide"
|
||||||
@ -33,10 +35,6 @@ ATTR_PM_10: Final = "particulate_matter_10"
|
|||||||
ATTR_PM_2_5: Final = "particulate_matter_2_5"
|
ATTR_PM_2_5: Final = "particulate_matter_2_5"
|
||||||
ATTR_SO2: Final = "sulphur_dioxide"
|
ATTR_SO2: Final = "sulphur_dioxide"
|
||||||
|
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
|
||||||
|
|
||||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
|
||||||
|
|
||||||
PROP_TO_ATTR: Final[dict[str, str]] = {
|
PROP_TO_ATTR: Final[dict[str, str]] = {
|
||||||
"air_quality_index": ATTR_AQI,
|
"air_quality_index": ATTR_AQI,
|
||||||
"carbon_dioxide": ATTR_CO2,
|
"carbon_dioxide": ATTR_CO2,
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from airgradient import AirGradientClient
|
from airgradient import AirGradientClient, get_model_name
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
@ -15,7 +15,14 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
Platform.UPDATE,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -29,7 +36,7 @@ class AirGradientData:
|
|||||||
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||||
"""Set up Airgradient from a config entry."""
|
"""Set up Airgradient from a config entry."""
|
||||||
|
|
||||||
client = AirGradientClient(
|
client = AirGradientClient(
|
||||||
@ -47,7 +54,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
||||||
manufacturer="AirGradient",
|
manufacturer="AirGradient",
|
||||||
model=measurement_coordinator.data.model,
|
model=get_model_name(measurement_coordinator.data.model),
|
||||||
|
model_id=measurement_coordinator.data.model,
|
||||||
serial_number=measurement_coordinator.data.serial_number,
|
serial_number=measurement_coordinator.data.serial_number,
|
||||||
sw_version=measurement_coordinator.data.firmware_version,
|
sw_version=measurement_coordinator.data.firmware_version,
|
||||||
)
|
)
|
||||||
@ -62,6 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AirGradientConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
104
homeassistant/components/airgradient/button.py
Normal file
104
homeassistant/components/airgradient/button.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"""Support for AirGradient buttons."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, ConfigurationControl
|
||||||
|
|
||||||
|
from homeassistant.components.button import (
|
||||||
|
DOMAIN as BUTTON_DOMAIN,
|
||||||
|
ButtonEntity,
|
||||||
|
ButtonEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import DOMAIN, AirGradientConfigEntry
|
||||||
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirGradientButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Describes AirGradient button entity."""
|
||||||
|
|
||||||
|
press_fn: Callable[[AirGradientClient], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
CO2_CALIBRATION = AirGradientButtonEntityDescription(
|
||||||
|
key="co2_calibration",
|
||||||
|
translation_key="co2_calibration",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
press_fn=lambda client: client.request_co2_calibration(),
|
||||||
|
)
|
||||||
|
LED_BAR_TEST = AirGradientButtonEntityDescription(
|
||||||
|
key="led_bar_test",
|
||||||
|
translation_key="led_bar_test",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
press_fn=lambda client: client.request_led_bar_test(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient button entities based on a config entry."""
|
||||||
|
model = entry.runtime_data.measurement.data.model
|
||||||
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _check_entities() -> None:
|
||||||
|
nonlocal added_entities
|
||||||
|
|
||||||
|
if (
|
||||||
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
|
and not added_entities
|
||||||
|
):
|
||||||
|
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
|
||||||
|
if "L" in model:
|
||||||
|
entities.append(AirGradientButton(coordinator, LED_BAR_TEST))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
added_entities = True
|
||||||
|
elif (
|
||||||
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
|
and added_entities
|
||||||
|
):
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
for entity_description in (CO2_CALIBRATION, LED_BAR_TEST):
|
||||||
|
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
BUTTON_DOMAIN, DOMAIN, unique_id
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
coordinator.async_add_listener(_check_entities)
|
||||||
|
_check_entities()
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientButton(AirGradientEntity, ButtonEntity):
|
||||||
|
"""Defines an AirGradient button."""
|
||||||
|
|
||||||
|
entity_description: AirGradientButtonEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirGradientConfigCoordinator,
|
||||||
|
description: AirGradientButtonEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize airgradient button."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
await self.entity_description.press_fn(self.coordinator.client)
|
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl
|
from airgradient import (
|
||||||
|
AirGradientClient,
|
||||||
|
AirGradientError,
|
||||||
|
AirGradientParseError,
|
||||||
|
ConfigurationControl,
|
||||||
|
)
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from mashumaro import MissingField
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
@ -83,10 +87,10 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
|
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
|
||||||
try:
|
try:
|
||||||
current_measures = await self.client.get_current_measures()
|
current_measures = await self.client.get_current_measures()
|
||||||
|
except AirGradientParseError:
|
||||||
|
return self.async_abort(reason="invalid_version")
|
||||||
except AirGradientError:
|
except AirGradientError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except MissingField:
|
|
||||||
return self.async_abort(reason="invalid_version")
|
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(current_measures.serial_number)
|
await self.async_set_unique_id(current_measures.serial_number)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from airgradient import PmStandard
|
||||||
|
|
||||||
DOMAIN = "airgradient"
|
DOMAIN = "airgradient"
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
PM_STANDARD = {
|
||||||
|
PmStandard.UGM3: "ugm3",
|
||||||
|
PmStandard.USAQI: "us_aqi",
|
||||||
|
}
|
||||||
|
PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()}
|
||||||
|
@ -19,7 +19,6 @@ if TYPE_CHECKING:
|
|||||||
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
_update_interval: timedelta
|
|
||||||
config_entry: AirGradientConfigEntry
|
config_entry: AirGradientConfigEntry
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||||
@ -28,7 +27,7 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
|||||||
hass,
|
hass,
|
||||||
logger=LOGGER,
|
logger=LOGGER,
|
||||||
name=f"AirGradient {client.host}",
|
name=f"AirGradient {client.host}",
|
||||||
update_interval=self._update_interval,
|
update_interval=timedelta(minutes=1),
|
||||||
)
|
)
|
||||||
self.client = client
|
self.client = client
|
||||||
assert self.config_entry.unique_id
|
assert self.config_entry.unique_id
|
||||||
@ -47,8 +46,6 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
|||||||
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
_update_interval = timedelta(minutes=1)
|
|
||||||
|
|
||||||
async def _update_data(self) -> Measures:
|
async def _update_data(self) -> Measures:
|
||||||
return await self.client.get_current_measures()
|
return await self.client.get_current_measures()
|
||||||
|
|
||||||
@ -56,7 +53,5 @@ class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
|||||||
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
_update_interval = timedelta(minutes=5)
|
|
||||||
|
|
||||||
async def _update_data(self) -> Config:
|
async def _update_data(self) -> Config:
|
||||||
return await self.client.get_config()
|
return await self.client.get_config()
|
||||||
|
@ -1,5 +1,41 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"co2_calibration": {
|
||||||
|
"default": "mdi:molecule-co2"
|
||||||
|
},
|
||||||
|
"led_bar_test": {
|
||||||
|
"default": "mdi:lightbulb-on-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"led_bar_brightness": {
|
||||||
|
"default": "mdi:brightness-percent"
|
||||||
|
},
|
||||||
|
"display_brightness": {
|
||||||
|
"default": "mdi:brightness-percent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"configuration_control": {
|
||||||
|
"default": "mdi:cloud-cog"
|
||||||
|
},
|
||||||
|
"display_temperature_unit": {
|
||||||
|
"default": "mdi:thermometer-lines"
|
||||||
|
},
|
||||||
|
"led_bar_mode": {
|
||||||
|
"default": "mdi:led-strip"
|
||||||
|
},
|
||||||
|
"nox_index_learning_time_offset": {
|
||||||
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"voc_index_learning_time_offset": {
|
||||||
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration": {
|
||||||
|
"default": "mdi:molecule-co2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"total_volatile_organic_component_index": {
|
"total_volatile_organic_component_index": {
|
||||||
"default": "mdi:molecule"
|
"default": "mdi:molecule"
|
||||||
@ -9,6 +45,32 @@
|
|||||||
},
|
},
|
||||||
"pm003_count": {
|
"pm003_count": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
|
"led_bar_brightness": {
|
||||||
|
"default": "mdi:brightness-percent"
|
||||||
|
},
|
||||||
|
"display_brightness": {
|
||||||
|
"default": "mdi:brightness-percent"
|
||||||
|
},
|
||||||
|
"display_temperature_unit": {
|
||||||
|
"default": "mdi:thermometer-lines"
|
||||||
|
},
|
||||||
|
"led_bar_mode": {
|
||||||
|
"default": "mdi:led-strip"
|
||||||
|
},
|
||||||
|
"nox_index_learning_time_offset": {
|
||||||
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"voc_index_learning_time_offset": {
|
||||||
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration": {
|
||||||
|
"default": "mdi:molecule-co2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"post_data_to_airgradient": {
|
||||||
|
"default": "mdi:cogs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airgradient==0.6.0"],
|
"requirements": ["airgradient==0.8.0"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
127
homeassistant/components/airgradient/number.py
Normal file
127
homeassistant/components/airgradient/number.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Support for AirGradient number entities."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, Config
|
||||||
|
from airgradient.models import ConfigurationControl
|
||||||
|
|
||||||
|
from homeassistant.components.number import (
|
||||||
|
DOMAIN as NUMBER_DOMAIN,
|
||||||
|
NumberEntity,
|
||||||
|
NumberEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirGradientConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirGradientNumberEntityDescription(NumberEntityDescription):
|
||||||
|
"""Describes AirGradient number entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Config], int]
|
||||||
|
set_value_fn: Callable[[AirGradientClient, int], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
DISPLAY_BRIGHTNESS = AirGradientNumberEntityDescription(
|
||||||
|
key="display_brightness",
|
||||||
|
translation_key="display_brightness",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=100,
|
||||||
|
native_step=1,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
value_fn=lambda config: config.display_brightness,
|
||||||
|
set_value_fn=lambda client, value: client.set_display_brightness(value),
|
||||||
|
)
|
||||||
|
|
||||||
|
LED_BAR_BRIGHTNESS = AirGradientNumberEntityDescription(
|
||||||
|
key="led_bar_brightness",
|
||||||
|
translation_key="led_bar_brightness",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=100,
|
||||||
|
native_step=1,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
value_fn=lambda config: config.led_bar_brightness,
|
||||||
|
set_value_fn=lambda client, value: client.set_led_bar_brightness(value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient number entities based on a config entry."""
|
||||||
|
|
||||||
|
model = entry.runtime_data.measurement.data.model
|
||||||
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_check_entities() -> None:
|
||||||
|
nonlocal added_entities
|
||||||
|
|
||||||
|
if (
|
||||||
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
|
and not added_entities
|
||||||
|
):
|
||||||
|
entities = []
|
||||||
|
if "I" in model:
|
||||||
|
entities.append(AirGradientNumber(coordinator, DISPLAY_BRIGHTNESS))
|
||||||
|
if "L" in model:
|
||||||
|
entities.append(AirGradientNumber(coordinator, LED_BAR_BRIGHTNESS))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
added_entities = True
|
||||||
|
elif (
|
||||||
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
|
and added_entities
|
||||||
|
):
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
for entity_description in (DISPLAY_BRIGHTNESS, LED_BAR_BRIGHTNESS):
|
||||||
|
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
NUMBER_DOMAIN, DOMAIN, unique_id
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
coordinator.async_add_listener(_async_check_entities)
|
||||||
|
_async_check_entities()
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientNumber(AirGradientEntity, NumberEntity):
|
||||||
|
"""Defines an AirGradient number entity."""
|
||||||
|
|
||||||
|
entity_description: AirGradientNumberEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirGradientConfigCoordinator,
|
||||||
|
description: AirGradientNumberEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AirGradient number."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> int | None:
|
||||||
|
"""Return the state of the number."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Set the selected value."""
|
||||||
|
await self.entity_description.set_value_fn(self.coordinator.client, int(value))
|
||||||
|
await self.coordinator.async_request_refresh()
|
@ -4,30 +4,23 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from airgradient import AirGradientClient, Config
|
from airgradient import AirGradientClient, Config
|
||||||
from airgradient.models import (
|
from airgradient.models import ConfigurationControl, LedBarMode, TemperatureUnit
|
||||||
ConfigurationControl,
|
|
||||||
LedBarMode,
|
|
||||||
PmStandard,
|
|
||||||
TemperatureUnit,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import (
|
||||||
|
DOMAIN as SELECT_DOMAIN,
|
||||||
|
SelectEntity,
|
||||||
|
SelectEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
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 AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
||||||
from .coordinator import AirGradientConfigCoordinator
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
PM_STANDARD = {
|
|
||||||
PmStandard.UGM3: "ugm3",
|
|
||||||
PmStandard.USAQI: "us_aqi",
|
|
||||||
}
|
|
||||||
PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AirGradientSelectEntityDescription(SelectEntityDescription):
|
class AirGradientSelectEntityDescription(SelectEntityDescription):
|
||||||
@ -35,8 +28,6 @@ class AirGradientSelectEntityDescription(SelectEntityDescription):
|
|||||||
|
|
||||||
value_fn: Callable[[Config], str | None]
|
value_fn: Callable[[Config], str | None]
|
||||||
set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]]
|
set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]]
|
||||||
requires_display: bool = False
|
|
||||||
requires_led_bar: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
|
CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
|
||||||
@ -54,7 +45,7 @@ CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
DISPLAY_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||||
AirGradientSelectEntityDescription(
|
AirGradientSelectEntityDescription(
|
||||||
key="display_temperature_unit",
|
key="display_temperature_unit",
|
||||||
translation_key="display_temperature_unit",
|
translation_key="display_temperature_unit",
|
||||||
@ -64,7 +55,6 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
|||||||
set_value_fn=lambda client, value: client.set_temperature_unit(
|
set_value_fn=lambda client, value: client.set_temperature_unit(
|
||||||
TemperatureUnit(value)
|
TemperatureUnit(value)
|
||||||
),
|
),
|
||||||
requires_display=True,
|
|
||||||
),
|
),
|
||||||
AirGradientSelectEntityDescription(
|
AirGradientSelectEntityDescription(
|
||||||
key="display_pm_standard",
|
key="display_pm_standard",
|
||||||
@ -75,8 +65,10 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
|||||||
set_value_fn=lambda client, value: client.set_pm_standard(
|
set_value_fn=lambda client, value: client.set_pm_standard(
|
||||||
PM_STANDARD_REVERSE[value]
|
PM_STANDARD_REVERSE[value]
|
||||||
),
|
),
|
||||||
requires_display=True,
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||||
AirGradientSelectEntityDescription(
|
AirGradientSelectEntityDescription(
|
||||||
key="led_bar_mode",
|
key="led_bar_mode",
|
||||||
translation_key="led_bar_mode",
|
translation_key="led_bar_mode",
|
||||||
@ -84,7 +76,63 @@ PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
|
|||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
value_fn=lambda config: config.led_bar_mode,
|
value_fn=lambda config: config.led_bar_mode,
|
||||||
set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)),
|
set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)),
|
||||||
requires_led_bar=True,
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
LEARNING_TIME_OFFSET_OPTIONS = [
|
||||||
|
"12",
|
||||||
|
"60",
|
||||||
|
"120",
|
||||||
|
"360",
|
||||||
|
"720",
|
||||||
|
]
|
||||||
|
|
||||||
|
ABC_DAYS = [
|
||||||
|
"1",
|
||||||
|
"8",
|
||||||
|
"30",
|
||||||
|
"90",
|
||||||
|
"180",
|
||||||
|
"0",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_value(value: int, values: list[str]) -> str | None:
|
||||||
|
str_value = str(value)
|
||||||
|
return str_value if str_value in values else None
|
||||||
|
|
||||||
|
|
||||||
|
CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="nox_index_learning_time_offset",
|
||||||
|
translation_key="nox_index_learning_time_offset",
|
||||||
|
options=LEARNING_TIME_OFFSET_OPTIONS,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: _get_value(
|
||||||
|
config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
|
||||||
|
),
|
||||||
|
set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)),
|
||||||
|
),
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="voc_index_learning_time_offset",
|
||||||
|
translation_key="voc_index_learning_time_offset",
|
||||||
|
options=LEARNING_TIME_OFFSET_OPTIONS,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: _get_value(
|
||||||
|
config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
|
||||||
|
),
|
||||||
|
set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)),
|
||||||
|
),
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="co2_automatic_baseline_calibration",
|
||||||
|
translation_key="co2_automatic_baseline_calibration",
|
||||||
|
options=ABC_DAYS,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: _get_value(
|
||||||
|
config.co2_automatic_baseline_calibration_days, ABC_DAYS
|
||||||
|
),
|
||||||
|
set_value_fn=lambda client,
|
||||||
|
value: client.set_co2_automatic_baseline_calibration(int(value)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,22 +144,57 @@ 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."""
|
||||||
|
|
||||||
config_coordinator = entry.runtime_data.config
|
coordinator = entry.runtime_data.config
|
||||||
measurement_coordinator = entry.runtime_data.measurement
|
measurement_coordinator = entry.runtime_data.measurement
|
||||||
|
|
||||||
entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)]
|
async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)])
|
||||||
|
|
||||||
|
model = measurement_coordinator.data.model
|
||||||
|
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_check_entities() -> None:
|
||||||
|
nonlocal added_entities
|
||||||
|
|
||||||
entities.extend(
|
|
||||||
AirGradientProtectedSelect(config_coordinator, description)
|
|
||||||
for description in PROTECTED_SELECT_TYPES
|
|
||||||
if (
|
if (
|
||||||
description.requires_display
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
and measurement_coordinator.data.model.startswith("I")
|
and not added_entities
|
||||||
)
|
):
|
||||||
or (description.requires_led_bar and "L" in measurement_coordinator.data.model)
|
entities: list[AirGradientSelect] = [
|
||||||
)
|
AirGradientSelect(coordinator, description)
|
||||||
|
for description in CONTROL_ENTITIES
|
||||||
|
]
|
||||||
|
if "I" in model:
|
||||||
|
entities.extend(
|
||||||
|
AirGradientSelect(coordinator, description)
|
||||||
|
for description in DISPLAY_SELECT_TYPES
|
||||||
|
)
|
||||||
|
if "L" in model:
|
||||||
|
entities.extend(
|
||||||
|
AirGradientSelect(coordinator, description)
|
||||||
|
for description in LED_BAR_ENTITIES
|
||||||
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
added_entities = True
|
||||||
|
elif (
|
||||||
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
|
and added_entities
|
||||||
|
):
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
for entity_description in (
|
||||||
|
DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES
|
||||||
|
):
|
||||||
|
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
SELECT_DOMAIN, DOMAIN, unique_id
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
coordinator.async_add_listener(_async_check_entities)
|
||||||
|
_async_check_entities()
|
||||||
|
|
||||||
|
|
||||||
class AirGradientSelect(AirGradientEntity, SelectEntity):
|
class AirGradientSelect(AirGradientEntity, SelectEntity):
|
||||||
@ -139,19 +222,3 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
|||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
await self.entity_description.set_value_fn(self.coordinator.client, option)
|
await self.entity_description.set_value_fn(self.coordinator.client, option)
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
|
||||||
class AirGradientProtectedSelect(AirGradientSelect):
|
|
||||||
"""Defines a protected AirGradient select entity."""
|
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
|
||||||
"""Change the selected option."""
|
|
||||||
if (
|
|
||||||
self.coordinator.data.configuration_control
|
|
||||||
is not ConfigurationControl.LOCAL
|
|
||||||
):
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="no_local_configuration",
|
|
||||||
)
|
|
||||||
await super().async_select_option(option)
|
|
||||||
|
@ -3,7 +3,13 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from airgradient.models import Measures
|
from airgradient import Config
|
||||||
|
from airgradient.models import (
|
||||||
|
ConfigurationControl,
|
||||||
|
LedBarMode,
|
||||||
|
Measures,
|
||||||
|
TemperatureUnit,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -18,60 +24,69 @@ from homeassistant.const import (
|
|||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
|
UnitOfTime,
|
||||||
)
|
)
|
||||||
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 homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .coordinator import AirGradientMeasurementCoordinator
|
from .const import PM_STANDARD, PM_STANDARD_REVERSE
|
||||||
|
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AirGradientSensorEntityDescription(SensorEntityDescription):
|
class AirGradientMeasurementSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes AirGradient sensor entity."""
|
"""Describes AirGradient measurement sensor entity."""
|
||||||
|
|
||||||
value_fn: Callable[[Measures], StateType]
|
value_fn: Callable[[Measures], StateType]
|
||||||
|
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
@dataclass(frozen=True, kw_only=True)
|
||||||
AirGradientSensorEntityDescription(
|
class AirGradientConfigSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes AirGradient config sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Config], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
MEASUREMENT_SENSOR_TYPES: tuple[AirGradientMeasurementSensorEntityDescription, ...] = (
|
||||||
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="pm01",
|
key="pm01",
|
||||||
device_class=SensorDeviceClass.PM1,
|
device_class=SensorDeviceClass.PM1,
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.pm01,
|
value_fn=lambda status: status.pm01,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="pm02",
|
key="pm02",
|
||||||
device_class=SensorDeviceClass.PM25,
|
device_class=SensorDeviceClass.PM25,
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.pm02,
|
value_fn=lambda status: status.pm02,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="pm10",
|
key="pm10",
|
||||||
device_class=SensorDeviceClass.PM10,
|
device_class=SensorDeviceClass.PM10,
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.pm10,
|
value_fn=lambda status: status.pm10,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.ambient_temperature,
|
value_fn=lambda status: status.ambient_temperature,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.relative_humidity,
|
value_fn=lambda status: status.relative_humidity,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="signal_strength",
|
key="signal_strength",
|
||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
@ -80,33 +95,33 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda status: status.signal_strength,
|
value_fn=lambda status: status.signal_strength,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="tvoc",
|
key="tvoc",
|
||||||
translation_key="total_volatile_organic_component_index",
|
translation_key="total_volatile_organic_component_index",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.total_volatile_organic_component_index,
|
value_fn=lambda status: status.total_volatile_organic_component_index,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="nitrogen_index",
|
key="nitrogen_index",
|
||||||
translation_key="nitrogen_index",
|
translation_key="nitrogen_index",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.nitrogen_index,
|
value_fn=lambda status: status.nitrogen_index,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="co2",
|
key="co2",
|
||||||
device_class=SensorDeviceClass.CO2,
|
device_class=SensorDeviceClass.CO2,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.rco2,
|
value_fn=lambda status: status.rco2,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="pm003",
|
key="pm003",
|
||||||
translation_key="pm003_count",
|
translation_key="pm003_count",
|
||||||
native_unit_of_measurement="particles/dL",
|
native_unit_of_measurement="particles/dL",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda status: status.pm003_count,
|
value_fn=lambda status: status.pm003_count,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="nox_raw",
|
key="nox_raw",
|
||||||
translation_key="raw_nitrogen",
|
translation_key="raw_nitrogen",
|
||||||
native_unit_of_measurement="ticks",
|
native_unit_of_measurement="ticks",
|
||||||
@ -114,7 +129,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda status: status.raw_nitrogen,
|
value_fn=lambda status: status.raw_nitrogen,
|
||||||
),
|
),
|
||||||
AirGradientSensorEntityDescription(
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
key="tvoc_raw",
|
key="tvoc_raw",
|
||||||
translation_key="raw_total_volatile_organic_component",
|
translation_key="raw_total_volatile_organic_component",
|
||||||
native_unit_of_measurement="ticks",
|
native_unit_of_measurement="ticks",
|
||||||
@ -124,6 +139,77 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONFIG_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = (
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="co2_automatic_baseline_calibration_days",
|
||||||
|
translation_key="co2_automatic_baseline_calibration_days",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.co2_automatic_baseline_calibration_days,
|
||||||
|
),
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="nox_learning_offset",
|
||||||
|
translation_key="nox_learning_offset",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.nox_learning_offset,
|
||||||
|
),
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="tvoc_learning_offset",
|
||||||
|
translation_key="tvoc_learning_offset",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.tvoc_learning_offset,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_LED_BAR_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = (
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="led_bar_mode",
|
||||||
|
translation_key="led_bar_mode",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[x.value for x in LedBarMode],
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.led_bar_mode,
|
||||||
|
),
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="led_bar_brightness",
|
||||||
|
translation_key="led_bar_brightness",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.led_bar_brightness,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_DISPLAY_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = (
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="display_temperature_unit",
|
||||||
|
translation_key="display_temperature_unit",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[x.value for x in TemperatureUnit],
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.temperature_unit,
|
||||||
|
),
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="display_pm_standard",
|
||||||
|
translation_key="display_pm_standard",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=list(PM_STANDARD_REVERSE),
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: PM_STANDARD.get(config.pm_standard),
|
||||||
|
),
|
||||||
|
AirGradientConfigSensorEntityDescription(
|
||||||
|
key="display_brightness",
|
||||||
|
translation_key="display_brightness",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda config: config.display_brightness,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -134,7 +220,9 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data.measurement
|
coordinator = entry.runtime_data.measurement
|
||||||
listener: Callable[[], None] | None = None
|
listener: Callable[[], None] | None = None
|
||||||
not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES)
|
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
|
||||||
|
MEASUREMENT_SENSOR_TYPES
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def add_entities() -> None:
|
def add_entities() -> None:
|
||||||
@ -147,7 +235,7 @@ async def async_setup_entry(
|
|||||||
if description.value_fn(coordinator.data) is None:
|
if description.value_fn(coordinator.data) is None:
|
||||||
not_setup.add(description)
|
not_setup.add(description)
|
||||||
else:
|
else:
|
||||||
sensors.append(AirGradientSensor(coordinator, description))
|
sensors.append(AirGradientMeasurementSensor(coordinator, description))
|
||||||
|
|
||||||
if sensors:
|
if sensors:
|
||||||
async_add_entities(sensors)
|
async_add_entities(sensors)
|
||||||
@ -159,17 +247,33 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
add_entities()
|
add_entities()
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
|
for description in CONFIG_SENSOR_TYPES
|
||||||
|
]
|
||||||
|
if "L" in coordinator.data.model:
|
||||||
|
entities.extend(
|
||||||
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
|
for description in CONFIG_LED_BAR_SENSOR_TYPES
|
||||||
|
)
|
||||||
|
if "I" in coordinator.data.model:
|
||||||
|
entities.extend(
|
||||||
|
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||||
|
for description in CONFIG_DISPLAY_SENSOR_TYPES
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
class AirGradientSensor(AirGradientEntity, SensorEntity):
|
|
||||||
|
class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity):
|
||||||
"""Defines an AirGradient sensor."""
|
"""Defines an AirGradient sensor."""
|
||||||
|
|
||||||
entity_description: AirGradientSensorEntityDescription
|
entity_description: AirGradientMeasurementSensorEntityDescription
|
||||||
coordinator: AirGradientMeasurementCoordinator
|
coordinator: AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AirGradientMeasurementCoordinator,
|
coordinator: AirGradientMeasurementCoordinator,
|
||||||
description: AirGradientSensorEntityDescription,
|
description: AirGradientMeasurementSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize airgradient sensor."""
|
"""Initialize airgradient sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
@ -180,3 +284,28 @@ class AirGradientSensor(AirGradientEntity, SensorEntity):
|
|||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientConfigSensor(AirGradientEntity, SensorEntity):
|
||||||
|
"""Defines an AirGradient sensor."""
|
||||||
|
|
||||||
|
entity_description: AirGradientConfigSensorEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirGradientConfigCoordinator,
|
||||||
|
description: AirGradientConfigSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize airgradient sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
self._attr_entity_registry_enabled_default = (
|
||||||
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
|
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@ -24,6 +25,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"co2_calibration": {
|
||||||
|
"name": "Calibrate CO2 sensor"
|
||||||
|
},
|
||||||
|
"led_bar_test": {
|
||||||
|
"name": "Test LED bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"led_bar_brightness": {
|
||||||
|
"name": "LED bar brightness"
|
||||||
|
},
|
||||||
|
"display_brightness": {
|
||||||
|
"name": "Display brightness"
|
||||||
|
}
|
||||||
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"configuration_control": {
|
"configuration_control": {
|
||||||
"name": "Configuration source",
|
"name": "Configuration source",
|
||||||
@ -53,6 +70,37 @@
|
|||||||
"co2": "Carbon dioxide",
|
"co2": "Carbon dioxide",
|
||||||
"pm": "Particulate matter"
|
"pm": "Particulate matter"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"nox_index_learning_time_offset": {
|
||||||
|
"name": "NOx index learning offset",
|
||||||
|
"state": {
|
||||||
|
"12": "12 hours",
|
||||||
|
"60": "60 hours",
|
||||||
|
"120": "120 hours",
|
||||||
|
"360": "360 hours",
|
||||||
|
"720": "720 hours"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"voc_index_learning_time_offset": {
|
||||||
|
"name": "VOC index learning offset",
|
||||||
|
"state": {
|
||||||
|
"12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]",
|
||||||
|
"60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]",
|
||||||
|
"120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]",
|
||||||
|
"360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]",
|
||||||
|
"720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration": {
|
||||||
|
"name": "CO2 automatic baseline duration",
|
||||||
|
"state": {
|
||||||
|
"1": "1 day",
|
||||||
|
"8": "8 days",
|
||||||
|
"30": "30 days",
|
||||||
|
"90": "90 days",
|
||||||
|
"180": "180 days",
|
||||||
|
"0": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
@ -70,12 +118,49 @@
|
|||||||
},
|
},
|
||||||
"raw_nitrogen": {
|
"raw_nitrogen": {
|
||||||
"name": "Raw NOx"
|
"name": "Raw NOx"
|
||||||
|
},
|
||||||
|
"display_pm_standard": {
|
||||||
|
"name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]",
|
||||||
|
"state": {
|
||||||
|
"ugm3": "[%key:component::airgradient::entity::select::display_pm_standard::state::ugm3%]",
|
||||||
|
"us_aqi": "[%key:component::airgradient::entity::select::display_pm_standard::state::us_aqi%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration_days": {
|
||||||
|
"name": "Carbon dioxide automatic baseline calibration"
|
||||||
|
},
|
||||||
|
"nox_learning_offset": {
|
||||||
|
"name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]"
|
||||||
|
},
|
||||||
|
"tvoc_learning_offset": {
|
||||||
|
"name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]"
|
||||||
|
},
|
||||||
|
"led_bar_mode": {
|
||||||
|
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
||||||
|
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
||||||
|
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"led_bar_brightness": {
|
||||||
|
"name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]"
|
||||||
|
},
|
||||||
|
"display_temperature_unit": {
|
||||||
|
"name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]",
|
||||||
|
"state": {
|
||||||
|
"c": "[%key:component::airgradient::entity::select::display_temperature_unit::state::c%]",
|
||||||
|
"f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"display_brightness": {
|
||||||
|
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switch": {
|
||||||
|
"post_data_to_airgradient": {
|
||||||
|
"name": "Post data to Airgradient"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"no_local_configuration": {
|
|
||||||
"message": "Device should be configured with local configuration to be able to change settings."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
110
homeassistant/components/airgradient/switch.py
Normal file
110
homeassistant/components/airgradient/switch.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""Support for AirGradient switch entities."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, Config
|
||||||
|
from airgradient.models import ConfigurationControl
|
||||||
|
|
||||||
|
from homeassistant.components.switch import (
|
||||||
|
DOMAIN as SWITCH_DOMAIN,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirGradientConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirGradientSwitchEntityDescription(SwitchEntityDescription):
|
||||||
|
"""Describes AirGradient switch entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Config], bool]
|
||||||
|
set_value_fn: Callable[[AirGradientClient, bool], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
POST_DATA_TO_AIRGRADIENT = AirGradientSwitchEntityDescription(
|
||||||
|
key="post_data_to_airgradient",
|
||||||
|
translation_key="post_data_to_airgradient",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: config.post_data_to_airgradient,
|
||||||
|
set_value_fn=lambda client, value: client.enable_sharing_data(enable=value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient switch entities based on a config entry."""
|
||||||
|
coordinator = entry.runtime_data.config
|
||||||
|
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_check_entities() -> None:
|
||||||
|
nonlocal added_entities
|
||||||
|
|
||||||
|
if (
|
||||||
|
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||||
|
and not added_entities
|
||||||
|
):
|
||||||
|
async_add_entities(
|
||||||
|
[AirGradientSwitch(coordinator, POST_DATA_TO_AIRGRADIENT)]
|
||||||
|
)
|
||||||
|
added_entities = True
|
||||||
|
elif (
|
||||||
|
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||||
|
and added_entities
|
||||||
|
):
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
unique_id = f"{coordinator.serial_number}-{POST_DATA_TO_AIRGRADIENT.key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
SWITCH_DOMAIN, DOMAIN, unique_id
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
added_entities = False
|
||||||
|
|
||||||
|
coordinator.async_add_listener(_async_check_entities)
|
||||||
|
_async_check_entities()
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
||||||
|
"""Defines an AirGradient switch entity."""
|
||||||
|
|
||||||
|
entity_description: AirGradientSwitchEntityDescription
|
||||||
|
coordinator: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirGradientConfigCoordinator,
|
||||||
|
description: AirGradientSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AirGradient switch."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the state of the switch."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
await self.entity_description.set_value_fn(self.coordinator.client, True)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off."""
|
||||||
|
await self.entity_description.set_value_fn(self.coordinator.client, False)
|
||||||
|
await self.coordinator.async_request_refresh()
|
55
homeassistant/components/airgradient/update.py
Normal file
55
homeassistant/components/airgradient/update.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"""Airgradient Update platform."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Airgradient update platform."""
|
||||||
|
|
||||||
|
data = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities([AirGradientUpdate(data.measurement)], True)
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||||
|
"""Representation of Airgradient Update."""
|
||||||
|
|
||||||
|
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||||
|
coordinator: AirGradientMeasurementCoordinator
|
||||||
|
|
||||||
|
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}-update"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return True because we need to poll the latest version."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def installed_version(self) -> str:
|
||||||
|
"""Return the installed version of the entity."""
|
||||||
|
return self.coordinator.data.firmware_version
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Update the entity."""
|
||||||
|
self._attr_latest_version = (
|
||||||
|
await self.coordinator.client.get_latest_firmware_version(
|
||||||
|
self.coordinator.serial_number
|
||||||
|
)
|
||||||
|
)
|
@ -156,7 +156,8 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
|||||||
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||||
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode == HVACMode.OFF:
|
||||||
return await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
return
|
||||||
await self._airtouch.SetCoolingModeForAc(
|
await self._airtouch.SetCoolingModeForAc(
|
||||||
self._ac_number, HA_STATE_TO_AT[hvac_mode]
|
self._ac_number, HA_STATE_TO_AT[hvac_mode]
|
||||||
)
|
)
|
||||||
@ -262,7 +263,8 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
|||||||
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||||
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
if hvac_mode == HVACMode.OFF:
|
||||||
return await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
return
|
||||||
if self.hvac_mode == HVACMode.OFF:
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
await self.async_turn_on()
|
await self.async_turn_on()
|
||||||
self._unit = self._airtouch.GetGroups()[self._group_number]
|
self._unit = self._airtouch.GetGroups()[self._group_number]
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
||||||
|
|
||||||
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
|
|||||||
"""Base class for Airtouch5 Climate Entities."""
|
"""Base class for Airtouch5 Climate Entities."""
|
||||||
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_translation_key = DOMAIN
|
||||||
_attr_target_temperature_step = 1
|
_attr_target_temperature_step = 1
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
_enable_turn_on_off_backwards_compatibility = False
|
||||||
|
134
homeassistant/components/airtouch5/cover.py
Normal file
134
homeassistant/components/airtouch5/cover.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""Representation of the Damper for AirTouch 5 Devices."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
|
||||||
|
from airtouch5py.packets.zone_control import (
|
||||||
|
ZoneControlZone,
|
||||||
|
ZoneSettingPower,
|
||||||
|
ZoneSettingValue,
|
||||||
|
)
|
||||||
|
from airtouch5py.packets.zone_name import ZoneName
|
||||||
|
from airtouch5py.packets.zone_status import ZoneStatusZone
|
||||||
|
|
||||||
|
from homeassistant.components.cover import (
|
||||||
|
ATTR_POSITION,
|
||||||
|
CoverDeviceClass,
|
||||||
|
CoverEntity,
|
||||||
|
CoverEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import Airtouch5ConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import Airtouch5Entity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: Airtouch5ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Airtouch 5 Cover entities."""
|
||||||
|
client = config_entry.runtime_data
|
||||||
|
|
||||||
|
# Each zone has a cover for its open percentage
|
||||||
|
async_add_entities(
|
||||||
|
Airtouch5ZoneOpenPercentage(
|
||||||
|
client, zone, client.latest_zone_status[zone.zone_number].has_sensor
|
||||||
|
)
|
||||||
|
for zone in client.zones
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Airtouch5ZoneOpenPercentage(CoverEntity, Airtouch5Entity):
|
||||||
|
"""How open the damper is in each zone."""
|
||||||
|
|
||||||
|
_attr_device_class = CoverDeviceClass.DAMPER
|
||||||
|
_attr_translation_key = "damper"
|
||||||
|
|
||||||
|
# Zones with temperature sensors shouldn't be manually controlled.
|
||||||
|
# We allow it but warn the user in the integration documentation.
|
||||||
|
_attr_supported_features = (
|
||||||
|
CoverEntityFeature.SET_POSITION
|
||||||
|
| CoverEntityFeature.OPEN
|
||||||
|
| CoverEntityFeature.CLOSE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, client: Airtouch5SimpleClient, zone_name: ZoneName, has_sensor: bool
|
||||||
|
) -> None:
|
||||||
|
"""Initialise the Cover Entity."""
|
||||||
|
super().__init__(client)
|
||||||
|
self._zone_name = zone_name
|
||||||
|
|
||||||
|
self._attr_unique_id = f"zone_{zone_name.zone_number}_open_percentage"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, f"zone_{zone_name.zone_number}")},
|
||||||
|
name=zone_name.zone_name,
|
||||||
|
manufacturer="Polyaire",
|
||||||
|
model="AirTouch 5",
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None:
|
||||||
|
if self._zone_name.zone_number not in data:
|
||||||
|
return
|
||||||
|
status = data[self._zone_name.zone_number]
|
||||||
|
|
||||||
|
self._attr_current_cover_position = int(status.open_percentage * 100)
|
||||||
|
if status.open_percentage == 0:
|
||||||
|
self._attr_is_closed = True
|
||||||
|
else:
|
||||||
|
self._attr_is_closed = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Add data updated listener after this object has been initialized."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._client.zone_status_callbacks.append(self._async_update_attrs)
|
||||||
|
self._async_update_attrs(self._client.latest_zone_status)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Remove data updated listener after this object has been initialized."""
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
self._client.zone_status_callbacks.remove(self._async_update_attrs)
|
||||||
|
|
||||||
|
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||||
|
"""Open the damper."""
|
||||||
|
await self._set_cover_position(100)
|
||||||
|
|
||||||
|
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||||
|
"""Close damper."""
|
||||||
|
await self._set_cover_position(0)
|
||||||
|
|
||||||
|
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||||
|
"""Update the damper to a specific position."""
|
||||||
|
|
||||||
|
if (position := kwargs.get(ATTR_POSITION)) is None:
|
||||||
|
_LOGGER.debug("Argument `position` is missing in set_cover_position")
|
||||||
|
return
|
||||||
|
await self._set_cover_position(position)
|
||||||
|
|
||||||
|
async def _set_cover_position(self, position_percent: float) -> None:
|
||||||
|
power: ZoneSettingPower
|
||||||
|
|
||||||
|
if position_percent == 0:
|
||||||
|
power = ZoneSettingPower.SET_TO_OFF
|
||||||
|
else:
|
||||||
|
power = ZoneSettingPower.SET_TO_ON
|
||||||
|
|
||||||
|
zcz = ZoneControlZone(
|
||||||
|
self._zone_name.zone_number,
|
||||||
|
ZoneSettingValue.SET_OPEN_PERCENTAGE,
|
||||||
|
power,
|
||||||
|
position_percent / 100.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = self._client.data_packet_factory.zone_control([zcz])
|
||||||
|
await self._client.send_packet(packet)
|
@ -6,15 +6,12 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class Airtouch5Entity(Entity):
|
class Airtouch5Entity(Entity):
|
||||||
"""Base class for Airtouch5 entities."""
|
"""Base class for Airtouch5 entities."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_translation_key = DOMAIN
|
|
||||||
|
|
||||||
def __init__(self, client: Airtouch5SimpleClient) -> None:
|
def __init__(self, client: Airtouch5SimpleClient) -> None:
|
||||||
"""Initialise the Entity."""
|
"""Initialise the Entity."""
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["airtouch5py"],
|
"loggers": ["airtouch5py"],
|
||||||
"requirements": ["airtouch5py==0.2.8"]
|
"requirements": ["airtouch5py==0.2.10"]
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cover": {
|
||||||
|
"damper": {
|
||||||
|
"name": "[%key:component::cover::entity_component::damper::name%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
aiohttp_client,
|
aiohttp_client,
|
||||||
config_validation as cv,
|
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
@ -62,8 +61,6 @@ PLATFORMS = [Platform.SENSOR]
|
|||||||
|
|
||||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_cloud_api_update_interval(
|
def async_get_cloud_api_update_interval(
|
||||||
|
@ -82,33 +82,54 @@ async def async_setup_entry(
|
|||||||
"""Add Airzone binary sensors from a config_entry."""
|
"""Add Airzone binary sensors from a config_entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
binary_sensors: list[AirzoneBinarySensor] = [
|
added_systems: set[str] = set()
|
||||||
AirzoneSystemBinarySensor(
|
added_zones: set[str] = set()
|
||||||
coordinator,
|
|
||||||
description,
|
|
||||||
entry,
|
|
||||||
system_id,
|
|
||||||
system_data,
|
|
||||||
)
|
|
||||||
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
|
|
||||||
for description in SYSTEM_BINARY_SENSOR_TYPES
|
|
||||||
if description.key in system_data
|
|
||||||
]
|
|
||||||
|
|
||||||
binary_sensors.extend(
|
def _async_entity_listener() -> None:
|
||||||
AirzoneZoneBinarySensor(
|
"""Handle additions of binary sensors."""
|
||||||
coordinator,
|
|
||||||
description,
|
|
||||||
entry,
|
|
||||||
system_zone_id,
|
|
||||||
zone_data,
|
|
||||||
)
|
|
||||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
|
||||||
for description in ZONE_BINARY_SENSOR_TYPES
|
|
||||||
if description.key in zone_data
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(binary_sensors)
|
entities: list[AirzoneBinarySensor] = []
|
||||||
|
|
||||||
|
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
|
||||||
|
received_systems = set(systems_data)
|
||||||
|
new_systems = received_systems - added_systems
|
||||||
|
if new_systems:
|
||||||
|
entities.extend(
|
||||||
|
AirzoneSystemBinarySensor(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_id,
|
||||||
|
systems_data.get(system_id),
|
||||||
|
)
|
||||||
|
for system_id in new_systems
|
||||||
|
for description in SYSTEM_BINARY_SENSOR_TYPES
|
||||||
|
if description.key in systems_data.get(system_id)
|
||||||
|
)
|
||||||
|
added_systems.update(new_systems)
|
||||||
|
|
||||||
|
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||||
|
received_zones = set(zones_data)
|
||||||
|
new_zones = received_zones - added_zones
|
||||||
|
if new_zones:
|
||||||
|
entities.extend(
|
||||||
|
AirzoneZoneBinarySensor(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
for description in ZONE_BINARY_SENSOR_TYPES
|
||||||
|
if description.key in zones_data.get(system_zone_id)
|
||||||
|
)
|
||||||
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
|
_async_entity_listener()
|
||||||
|
|
||||||
|
|
||||||
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
|
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
|
||||||
|
@ -102,17 +102,31 @@ async def async_setup_entry(
|
|||||||
entry: AirzoneConfigEntry,
|
entry: AirzoneConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add Airzone sensors from a config_entry."""
|
"""Add Airzone climate from a config_entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
|
||||||
AirzoneClimate(
|
added_zones: set[str] = set()
|
||||||
coordinator,
|
|
||||||
entry,
|
def _async_entity_listener() -> None:
|
||||||
system_zone_id,
|
"""Handle additions of climate."""
|
||||||
zone_data,
|
|
||||||
)
|
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
received_zones = set(zones_data)
|
||||||
)
|
new_zones = received_zones - added_zones
|
||||||
|
if new_zones:
|
||||||
|
async_add_entities(
|
||||||
|
AirzoneClimate(
|
||||||
|
coordinator,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
)
|
||||||
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
|
_async_entity_listener()
|
||||||
|
|
||||||
|
|
||||||
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||||
|
@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await airzone.get_version()
|
await airzone.get_version()
|
||||||
except AirzoneError as err:
|
except (AirzoneError, TimeoutError) as err:
|
||||||
raise AbortFlow("cannot_connect") from err
|
raise AbortFlow("cannot_connect") from err
|
||||||
|
|
||||||
return await self.async_step_discovered_connection()
|
return await self.async_step_discovered_connection()
|
||||||
|
@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.7.7"]
|
"requirements": ["aioairzone==0.8.2"]
|
||||||
}
|
}
|
||||||
|
@ -83,21 +83,34 @@ async def async_setup_entry(
|
|||||||
entry: AirzoneConfigEntry,
|
entry: AirzoneConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add Airzone sensors from a config_entry."""
|
"""Add Airzone select from a config_entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
added_zones: set[str] = set()
|
||||||
AirzoneZoneSelect(
|
|
||||||
coordinator,
|
def _async_entity_listener() -> None:
|
||||||
description,
|
"""Handle additions of select."""
|
||||||
entry,
|
|
||||||
system_zone_id,
|
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||||
zone_data,
|
received_zones = set(zones_data)
|
||||||
)
|
new_zones = received_zones - added_zones
|
||||||
for description in ZONE_SELECT_TYPES
|
if new_zones:
|
||||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
async_add_entities(
|
||||||
if description.key in zone_data
|
AirzoneZoneSelect(
|
||||||
)
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
for description in ZONE_SELECT_TYPES
|
||||||
|
if description.key in zones_data.get(system_zone_id)
|
||||||
|
)
|
||||||
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
|
_async_entity_listener()
|
||||||
|
|
||||||
|
|
||||||
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||||
|
@ -85,21 +85,37 @@ async def async_setup_entry(
|
|||||||
"""Add Airzone sensors from a config_entry."""
|
"""Add Airzone sensors from a config_entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
sensors: list[AirzoneSensor] = [
|
added_zones: set[str] = set()
|
||||||
AirzoneZoneSensor(
|
|
||||||
coordinator,
|
def _async_entity_listener() -> None:
|
||||||
description,
|
"""Handle additions of sensors."""
|
||||||
entry,
|
|
||||||
system_zone_id,
|
entities: list[AirzoneSensor] = []
|
||||||
zone_data,
|
|
||||||
)
|
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
received_zones = set(zones_data)
|
||||||
for description in ZONE_SENSOR_TYPES
|
new_zones = received_zones - added_zones
|
||||||
if description.key in zone_data
|
if new_zones:
|
||||||
]
|
entities.extend(
|
||||||
|
AirzoneZoneSensor(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
for description in ZONE_SENSOR_TYPES
|
||||||
|
if description.key in zones_data.get(system_zone_id)
|
||||||
|
)
|
||||||
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
entities: list[AirzoneSensor] = []
|
||||||
|
|
||||||
if AZD_HOT_WATER in coordinator.data:
|
if AZD_HOT_WATER in coordinator.data:
|
||||||
sensors.extend(
|
entities.extend(
|
||||||
AirzoneHotWaterSensor(
|
AirzoneHotWaterSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
@ -110,7 +126,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if AZD_WEBSERVER in coordinator.data:
|
if AZD_WEBSERVER in coordinator.data:
|
||||||
sensors.extend(
|
entities.extend(
|
||||||
AirzoneWebServerSensor(
|
AirzoneWebServerSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
@ -120,7 +136,10 @@ async def async_setup_entry(
|
|||||||
if description.key in coordinator.data[AZD_WEBSERVER]
|
if description.key in coordinator.data[AZD_WEBSERVER]
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(sensors)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
|
_async_entity_listener()
|
||||||
|
|
||||||
|
|
||||||
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||||
|
@ -61,7 +61,7 @@ async def async_setup_entry(
|
|||||||
entry: AirzoneConfigEntry,
|
entry: AirzoneConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add Airzone sensors from a config_entry."""
|
"""Add Airzone Water Heater from a config_entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
if AZD_HOT_WATER in coordinator.data:
|
if AZD_HOT_WATER in coordinator.data:
|
||||||
async_add_entities([AirzoneWaterHeater(coordinator, entry)])
|
async_add_entities([AirzoneWaterHeater(coordinator, entry)])
|
||||||
|
@ -14,6 +14,7 @@ from aioairzone_cloud.const import (
|
|||||||
AZD_FLOOR_DEMAND,
|
AZD_FLOOR_DEMAND,
|
||||||
AZD_PROBLEMS,
|
AZD_PROBLEMS,
|
||||||
AZD_SYSTEMS,
|
AZD_SYSTEMS,
|
||||||
|
AZD_THERMOSTAT_BATTERY_LOW,
|
||||||
AZD_WARNINGS,
|
AZD_WARNINGS,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
)
|
)
|
||||||
@ -88,6 +89,10 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]
|
|||||||
key=AZD_AQ_ACTIVE,
|
key=AZD_AQ_ACTIVE,
|
||||||
translation_key="air_quality_active",
|
translation_key="air_quality_active",
|
||||||
),
|
),
|
||||||
|
AirzoneBinarySensorEntityDescription(
|
||||||
|
device_class=BinarySensorDeviceClass.BATTERY,
|
||||||
|
key=AZD_THERMOSTAT_BATTERY_LOW,
|
||||||
|
),
|
||||||
AirzoneBinarySensorEntityDescription(
|
AirzoneBinarySensorEntityDescription(
|
||||||
device_class=BinarySensorDeviceClass.RUNNING,
|
device_class=BinarySensorDeviceClass.RUNNING,
|
||||||
key=AZD_FLOOR_DEMAND,
|
key=AZD_FLOOR_DEMAND,
|
||||||
@ -156,6 +161,11 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
|
|||||||
|
|
||||||
entity_description: AirzoneBinarySensorEntityDescription
|
entity_description: AirzoneBinarySensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return Airzone Cloud binary sensor availability."""
|
||||||
|
return super().available and self.is_on is not None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Update attributes when the coordinator updates."""
|
"""Update attributes when the coordinator updates."""
|
||||||
|
@ -13,9 +13,12 @@ from aioairzone_cloud.const import (
|
|||||||
AZD_GROUPS,
|
AZD_GROUPS,
|
||||||
AZD_HOT_WATERS,
|
AZD_HOT_WATERS,
|
||||||
AZD_INSTALLATIONS,
|
AZD_INSTALLATIONS,
|
||||||
|
AZD_MODEL,
|
||||||
AZD_NAME,
|
AZD_NAME,
|
||||||
AZD_SYSTEM_ID,
|
AZD_SYSTEM_ID,
|
||||||
AZD_SYSTEMS,
|
AZD_SYSTEMS,
|
||||||
|
AZD_THERMOSTAT_FW,
|
||||||
|
AZD_THERMOSTAT_MODEL,
|
||||||
AZD_WEBSERVER,
|
AZD_WEBSERVER,
|
||||||
AZD_WEBSERVERS,
|
AZD_WEBSERVERS,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
@ -69,6 +72,7 @@ class AirzoneAidooEntity(AirzoneEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, aidoo_id)},
|
identifiers={(DOMAIN, aidoo_id)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
|
model=aidoo_data[AZD_MODEL],
|
||||||
name=aidoo_data[AZD_NAME],
|
name=aidoo_data[AZD_NAME],
|
||||||
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
|
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
|
||||||
)
|
)
|
||||||
@ -111,6 +115,7 @@ class AirzoneGroupEntity(AirzoneEntity):
|
|||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, group_id)},
|
identifiers={(DOMAIN, group_id)},
|
||||||
|
model="Group",
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=group_data[AZD_NAME],
|
name=group_data[AZD_NAME],
|
||||||
)
|
)
|
||||||
@ -154,6 +159,7 @@ class AirzoneHotWaterEntity(AirzoneEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, dhw_id)},
|
identifiers={(DOMAIN, dhw_id)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
|
model="Hot Water",
|
||||||
name=dhw_data[AZD_NAME],
|
name=dhw_data[AZD_NAME],
|
||||||
via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]),
|
via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]),
|
||||||
)
|
)
|
||||||
@ -195,6 +201,7 @@ class AirzoneInstallationEntity(AirzoneEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, inst_id)},
|
identifiers={(DOMAIN, inst_id)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
|
model="Installation",
|
||||||
name=inst_data[AZD_NAME],
|
name=inst_data[AZD_NAME],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -240,9 +247,11 @@ class AirzoneSystemEntity(AirzoneEntity):
|
|||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, system_id)},
|
identifiers={(DOMAIN, system_id)},
|
||||||
|
model=system_data.get(AZD_MODEL),
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=system_data[AZD_NAME],
|
name=system_data[AZD_NAME],
|
||||||
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
|
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
|
||||||
|
sw_version=system_data.get(AZD_FIRMWARE),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_airzone_value(self, key: str) -> Any:
|
def get_airzone_value(self, key: str) -> Any:
|
||||||
@ -270,6 +279,7 @@ class AirzoneWebServerEntity(AirzoneEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
|
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
|
||||||
identifiers={(DOMAIN, ws_id)},
|
identifiers={(DOMAIN, ws_id)},
|
||||||
|
model="WebServer",
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=ws_data[AZD_NAME],
|
name=ws_data[AZD_NAME],
|
||||||
sw_version=ws_data[AZD_FIRMWARE],
|
sw_version=ws_data[AZD_FIRMWARE],
|
||||||
@ -300,9 +310,11 @@ class AirzoneZoneEntity(AirzoneEntity):
|
|||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, zone_id)},
|
identifiers={(DOMAIN, zone_id)},
|
||||||
|
model=zone_data.get(AZD_THERMOSTAT_MODEL),
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=zone_data[AZD_NAME],
|
name=zone_data[AZD_NAME],
|
||||||
via_device=(DOMAIN, self.system_id),
|
via_device=(DOMAIN, self.system_id),
|
||||||
|
sw_version=zone_data.get(AZD_THERMOSTAT_FW),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_airzone_value(self, key: str) -> Any:
|
def get_airzone_value(self, key: str) -> Any:
|
||||||
|
15
homeassistant/components/airzone_cloud/icons.json
Normal file
15
homeassistant/components/airzone_cloud/icons.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"cpu_usage": {
|
||||||
|
"default": "mdi:cpu-32-bit"
|
||||||
|
},
|
||||||
|
"free_memory": {
|
||||||
|
"default": "mdi:memory"
|
||||||
|
},
|
||||||
|
"thermostat_coverage": {
|
||||||
|
"default": "mdi:signal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.5.3"]
|
"requirements": ["aioairzone-cloud==0.6.2"]
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,12 @@ from aioairzone_cloud.const import (
|
|||||||
AZD_AQ_PM_1,
|
AZD_AQ_PM_1,
|
||||||
AZD_AQ_PM_2P5,
|
AZD_AQ_PM_2P5,
|
||||||
AZD_AQ_PM_10,
|
AZD_AQ_PM_10,
|
||||||
|
AZD_CPU_USAGE,
|
||||||
AZD_HUMIDITY,
|
AZD_HUMIDITY,
|
||||||
|
AZD_MEMORY_FREE,
|
||||||
AZD_TEMP,
|
AZD_TEMP,
|
||||||
|
AZD_THERMOSTAT_BATTERY,
|
||||||
|
AZD_THERMOSTAT_COVERAGE,
|
||||||
AZD_WEBSERVERS,
|
AZD_WEBSERVERS,
|
||||||
AZD_WIFI_RSSI,
|
AZD_WIFI_RSSI,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
@ -28,6 +32,7 @@ from homeassistant.const import (
|
|||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
|
UnitOfInformation,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -52,6 +57,22 @@ AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
key=AZD_CPU_USAGE,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
translation_key="cpu_usage",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
key=AZD_MEMORY_FREE,
|
||||||
|
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
translation_key="free_memory",
|
||||||
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -98,6 +119,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
|||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
key=AZD_THERMOSTAT_BATTERY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
key=AZD_THERMOSTAT_COVERAGE,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
translation_key="thermostat_coverage",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -154,6 +189,11 @@ async def async_setup_entry(
|
|||||||
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||||
"""Define an Airzone Cloud sensor."""
|
"""Define an Airzone Cloud sensor."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return Airzone Cloud sensor availability."""
|
||||||
|
return super().available and self.native_value is not None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Update attributes when the coordinator updates."""
|
"""Update attributes when the coordinator updates."""
|
||||||
|
@ -37,6 +37,17 @@
|
|||||||
"auto": "Auto"
|
"auto": "Auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"cpu_usage": {
|
||||||
|
"name": "CPU usage"
|
||||||
|
},
|
||||||
|
"free_memory": {
|
||||||
|
"name": "Free memory"
|
||||||
|
},
|
||||||
|
"thermostat_coverage": {
|
||||||
|
"name": "Signal percentage"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,93 +2,37 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
|
||||||
OAuth2Session,
|
|
||||||
async_get_config_entry_implementation,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
DOMAIN = "aladdin_connect"
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AladdinConnectCoordinator
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
|
|
||||||
|
|
||||||
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
"""Set up Aladdin Connect from a config entry."""
|
||||||
) -> bool:
|
ir.async_create_issue(
|
||||||
"""Set up Aladdin Connect Genie from a config entry."""
|
hass,
|
||||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
session = OAuth2Session(hass, entry, implementation)
|
is_fixable=False,
|
||||||
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
|
severity=ir.IssueSeverity.ERROR,
|
||||||
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth))
|
translation_key="integration_removed",
|
||||||
|
translation_placeholders={
|
||||||
await coordinator.async_setup()
|
"entries": "/config/integrations/integration/aladdin_connect",
|
||||||
await coordinator.async_config_entry_first_refresh()
|
},
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
async_remove_stale_devices(hass, entry)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
|
||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(
|
|
||||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Migrate old config."""
|
|
||||||
if config_entry.version < 2:
|
|
||||||
config_entry.async_start_reauth(hass)
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
config_entry,
|
|
||||||
version=2,
|
|
||||||
minor_version=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def async_remove_stale_devices(
|
|
||||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
|
||||||
) -> None:
|
|
||||||
"""Remove stale devices from device registry."""
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
device_entries = dr.async_entries_for_config_entry(
|
|
||||||
device_registry, config_entry.entry_id
|
|
||||||
)
|
)
|
||||||
all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors}
|
|
||||||
|
|
||||||
for device_entry in device_entries:
|
return True
|
||||||
device_id: str | None = None
|
|
||||||
|
|
||||||
for identifier in device_entry.identifiers:
|
|
||||||
if identifier[0] == DOMAIN:
|
|
||||||
device_id = identifier[1]
|
|
||||||
break
|
|
||||||
|
|
||||||
if device_id is None or device_id not in all_device_ids:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
# If device_id is None an invalid device entry was found for this config entry.
|
"""Unload a config entry."""
|
||||||
# If the device_id is not in existing device ids it's a stale device entry.
|
if all(
|
||||||
# Remove config entry from this device entry in either case.
|
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
device_registry.async_update_device(
|
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
if config_entry.entry_id != entry.entry_id
|
||||||
)
|
):
|
||||||
|
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
from genie_partner_sdk.auth import Auth
|
|
||||||
|
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
|
||||||
|
|
||||||
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
|
|
||||||
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
|
|
||||||
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
websession: ClientSession,
|
|
||||||
oauth_session: OAuth2Session,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize Aladdin Connect Genie auth."""
|
|
||||||
super().__init__(
|
|
||||||
websession, API_URL, oauth_session.token["access_token"], API_KEY
|
|
||||||
)
|
|
||||||
self._oauth_session = oauth_session
|
|
||||||
|
|
||||||
async def async_get_access_token(self) -> str:
|
|
||||||
"""Return a valid access token."""
|
|
||||||
await self._oauth_session.async_ensure_token_valid()
|
|
||||||
|
|
||||||
return cast(str, self._oauth_session.token["access_token"])
|
|
@ -1,70 +1,11 @@
|
|||||||
"""Config flow for Aladdin Connect Genie."""
|
"""Config flow for Aladdin Connect integration."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from homeassistant.config_entries import ConfigFlow
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import jwt
|
from . import DOMAIN
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
|
"""Handle a config flow for Aladdin Connect."""
|
||||||
|
|
||||||
DOMAIN = DOMAIN
|
VERSION = 1
|
||||||
VERSION = 2
|
|
||||||
MINOR_VERSION = 1
|
|
||||||
|
|
||||||
reauth_entry: ConfigEntry | None = None
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, user_input: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
|
|
||||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
|
||||||
self.context["entry_id"]
|
|
||||||
)
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: Mapping[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Dialog that informs the user that reauth is required."""
|
|
||||||
if user_input is None:
|
|
||||||
return self.async_show_form(step_id="reauth_confirm")
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
|
||||||
"""Create an oauth config entry or update existing entry for reauth."""
|
|
||||||
token_payload = jwt.decode(
|
|
||||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False}
|
|
||||||
)
|
|
||||||
if not self.reauth_entry:
|
|
||||||
await self.async_set_unique_id(token_payload["sub"])
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=token_payload["username"],
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.reauth_entry.unique_id == token_payload["username"]:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self.reauth_entry,
|
|
||||||
data=data,
|
|
||||||
unique_id=token_payload["sub"],
|
|
||||||
)
|
|
||||||
if self.reauth_entry.unique_id == token_payload["sub"]:
|
|
||||||
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
|
|
||||||
|
|
||||||
return self.async_abort(reason="wrong_account")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def logger(self) -> logging.Logger:
|
|
||||||
"""Return logger."""
|
|
||||||
return logging.getLogger(__name__)
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
"""Constants for the Aladdin Connect Genie integration."""
|
|
||||||
|
|
||||||
DOMAIN = "aladdin_connect"
|
|
||||||
|
|
||||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
|
|
||||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
|
@ -1,38 +0,0 @@
|
|||||||
"""Define an object to coordinate fetching Aladdin Connect data."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectCoordinator(DataUpdateCoordinator[None]):
|
|
||||||
"""Aladdin Connect Data Update Coordinator."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None:
|
|
||||||
"""Initialize."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
logger=_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_interval=timedelta(seconds=15),
|
|
||||||
)
|
|
||||||
self.acc = acc
|
|
||||||
self.doors: list[GarageDoor] = []
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
|
||||||
"""Fetch initial data."""
|
|
||||||
self.doors = await self.acc.get_doors()
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
|
||||||
"""Fetch data from API endpoint."""
|
|
||||||
for door in self.doors:
|
|
||||||
await self.acc.update_door(door.device_id, door.door_number)
|
|
@ -1,84 +0,0 @@
|
|||||||
"""Cover Entity for Genie Garage Door."""
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
|
||||||
CoverDeviceClass,
|
|
||||||
CoverEntity,
|
|
||||||
CoverEntityFeature,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
|
||||||
from .entity import AladdinConnectEntity
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: AladdinConnectConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Aladdin Connect platform."""
|
|
||||||
coordinator = config_entry.runtime_data
|
|
||||||
|
|
||||||
async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinDevice(AladdinConnectEntity, CoverEntity):
|
|
||||||
"""Representation of Aladdin Connect cover."""
|
|
||||||
|
|
||||||
_attr_device_class = CoverDeviceClass.GARAGE
|
|
||||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
|
||||||
_attr_name = None
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Aladdin Connect cover."""
|
|
||||||
super().__init__(coordinator, device)
|
|
||||||
self._attr_unique_id = device.unique_id
|
|
||||||
|
|
||||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
|
||||||
"""Issue open command to cover."""
|
|
||||||
await self.coordinator.acc.open_door(
|
|
||||||
self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
|
||||||
"""Issue close command to cover."""
|
|
||||||
await self.coordinator.acc.close_door(
|
|
||||||
self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closed(self) -> bool | None:
|
|
||||||
"""Update is closed attribute."""
|
|
||||||
value = self.coordinator.acc.get_door_status(
|
|
||||||
self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return bool(value == "closed")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closing(self) -> bool | None:
|
|
||||||
"""Update is closing attribute."""
|
|
||||||
value = self.coordinator.acc.get_door_status(
|
|
||||||
self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return bool(value == "closing")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_opening(self) -> bool | None:
|
|
||||||
"""Update is opening attribute."""
|
|
||||||
value = self.coordinator.acc.get_door_status(
|
|
||||||
self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return bool(value == "opening")
|
|
@ -1,27 +0,0 @@
|
|||||||
"""Defines a base Aladdin Connect entity."""
|
|
||||||
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AladdinConnectCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
|
||||||
"""Defines a base Aladdin Connect entity."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._device = device
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, device.unique_id)},
|
|
||||||
name=device.name,
|
|
||||||
manufacturer="Overhead Door",
|
|
||||||
)
|
|
@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "aladdin_connect",
|
"domain": "aladdin_connect",
|
||||||
"name": "Aladdin Connect",
|
"name": "Aladdin Connect",
|
||||||
"codeowners": ["@swcloudgenie"],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": ["application_credentials"],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||||
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["genie-partner-sdk==1.0.2"]
|
"requirements": []
|
||||||
}
|
}
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
"""Support for Aladdin Connect Garage Door sensors."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import PERCENTAGE
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
|
||||||
from .entity import AladdinConnectEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class AccSensorEntityDescription(SensorEntityDescription):
|
|
||||||
"""Describes AladdinConnect sensor entity."""
|
|
||||||
|
|
||||||
value_fn: Callable[[AladdinConnectClient, str, int], float | None]
|
|
||||||
|
|
||||||
|
|
||||||
SENSORS: tuple[AccSensorEntityDescription, ...] = (
|
|
||||||
AccSensorEntityDescription(
|
|
||||||
key="battery_level",
|
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=AladdinConnectClient.get_battery_status,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AladdinConnectConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Aladdin Connect sensor devices."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AladdinConnectSensor(coordinator, door, description)
|
|
||||||
for description in SENSORS
|
|
||||||
for door in coordinator.doors
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
|
||||||
"""A sensor implementation for Aladdin Connect devices."""
|
|
||||||
|
|
||||||
entity_description: AccSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AladdinConnectCoordinator,
|
|
||||||
device: GarageDoor,
|
|
||||||
description: AccSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize a sensor for an Aladdin Connect device."""
|
|
||||||
super().__init__(coordinator, device)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = f"{device.unique_id}-{description.key}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
return self.entity_description.value_fn(
|
|
||||||
self.coordinator.acc, self._device.device_id, self._device.door_number
|
|
||||||
)
|
|
@ -1,29 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"issues": {
|
||||||
"step": {
|
"integration_removed": {
|
||||||
"pick_implementation": {
|
"title": "The Aladdin Connect integration has been removed",
|
||||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
|
||||||
},
|
|
||||||
"reauth_confirm": {
|
|
||||||
"title": "[%key:common::config_flow::title::reauth%]",
|
|
||||||
"description": "Aladdin Connect needs to re-authenticate your account"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
|
||||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
|
||||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
|
||||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
|
||||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
|
||||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
|
||||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
|
||||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
|
||||||
},
|
|
||||||
"create_entry": {
|
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,10 @@ from .const import ( # noqa: F401
|
|||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
|
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
||||||
|
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
||||||
|
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||||
|
|
||||||
CONF_DEFAULT_CODE = "default_code"
|
CONF_DEFAULT_CODE = "default_code"
|
||||||
|
|
||||||
@ -61,8 +63,6 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
|
|||||||
{vol.Optional(ATTR_CODE): cv.string}
|
{vol.Optional(ATTR_CODE): cv.string}
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
|
||||||
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
|
||||||
|
|
||||||
# mypy: disallow-any-generics
|
# mypy: disallow-any-generics
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
# Describes the format for available alarm control panel services
|
# Describes the format for available alarm control panel services
|
||||||
|
.common_service_fields: &common_service_fields
|
||||||
|
code:
|
||||||
|
example: "1234"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|
||||||
alarm_disarm:
|
alarm_disarm:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_arm_custom_bypass:
|
alarm_arm_custom_bypass:
|
||||||
target:
|
target:
|
||||||
@ -16,11 +17,7 @@ alarm_arm_custom_bypass:
|
|||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_arm_home:
|
alarm_arm_home:
|
||||||
target:
|
target:
|
||||||
@ -28,11 +25,7 @@ alarm_arm_home:
|
|||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_arm_away:
|
alarm_arm_away:
|
||||||
target:
|
target:
|
||||||
@ -40,23 +33,14 @@ alarm_arm_away:
|
|||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_arm_night:
|
alarm_arm_night:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_arm_vacation:
|
alarm_arm_vacation:
|
||||||
target:
|
target:
|
||||||
@ -64,11 +48,7 @@ alarm_arm_vacation:
|
|||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
alarm_trigger:
|
alarm_trigger:
|
||||||
target:
|
target:
|
||||||
@ -76,8 +56,4 @@ alarm_trigger:
|
|||||||
domain: alarm_control_panel
|
domain: alarm_control_panel
|
||||||
supported_features:
|
supported_features:
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER
|
- alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER
|
||||||
fields:
|
fields: *common_service_fields
|
||||||
code:
|
|
||||||
example: "1234"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
@ -124,9 +124,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
if not entities:
|
if not entities:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off")
|
||||||
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
|
component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")
|
||||||
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle")
|
||||||
|
|
||||||
await component.async_add_entities(entities)
|
await component.async_add_entities(entities)
|
||||||
|
|
||||||
@ -162,16 +162,8 @@ class Alert(Entity):
|
|||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
self._message_template = message_template
|
self._message_template = message_template
|
||||||
if self._message_template is not None:
|
|
||||||
self._message_template.hass = hass
|
|
||||||
|
|
||||||
self._done_message_template = done_message_template
|
self._done_message_template = done_message_template
|
||||||
if self._done_message_template is not None:
|
|
||||||
self._done_message_template.hass = hass
|
|
||||||
|
|
||||||
self._title_template = title_template
|
self._title_template = title_template
|
||||||
if self._title_template is not None:
|
|
||||||
self._title_template.hass = hass
|
|
||||||
|
|
||||||
self._notifiers = notifiers
|
self._notifiers = notifiers
|
||||||
self._can_ack = can_ack
|
self._can_ack = can_ack
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from typing_extensions import Generator
|
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
button,
|
button,
|
||||||
climate,
|
climate,
|
||||||
@ -19,6 +18,7 @@ from homeassistant.components import (
|
|||||||
light,
|
light,
|
||||||
media_player,
|
media_player,
|
||||||
number,
|
number,
|
||||||
|
remote,
|
||||||
timer,
|
timer,
|
||||||
vacuum,
|
vacuum,
|
||||||
valve,
|
valve,
|
||||||
@ -439,6 +439,8 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
is_on = self.entity.state == fan.STATE_ON
|
is_on = self.entity.state == fan.STATE_ON
|
||||||
elif self.entity.domain == humidifier.DOMAIN:
|
elif self.entity.domain == humidifier.DOMAIN:
|
||||||
is_on = self.entity.state == humidifier.STATE_ON
|
is_on = self.entity.state == humidifier.STATE_ON
|
||||||
|
elif self.entity.domain == remote.DOMAIN:
|
||||||
|
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||||
elif self.entity.domain == vacuum.DOMAIN:
|
elif self.entity.domain == vacuum.DOMAIN:
|
||||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||||
elif self.entity.domain == timer.DOMAIN:
|
elif self.entity.domain == timer.DOMAIN:
|
||||||
@ -1436,6 +1438,12 @@ class AlexaModeController(AlexaCapability):
|
|||||||
if mode in modes:
|
if mode in modes:
|
||||||
return f"{humidifier.ATTR_MODE}.{mode}"
|
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||||
|
|
||||||
|
# Remote Activity
|
||||||
|
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||||
|
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
|
||||||
|
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
|
||||||
|
return f"{remote.ATTR_ACTIVITY}.{activity}"
|
||||||
|
|
||||||
# Water heater operation mode
|
# Water heater operation mode
|
||||||
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||||
operation_mode = self.entity.attributes.get(
|
operation_mode = self.entity.attributes.get(
|
||||||
@ -1550,6 +1558,24 @@ class AlexaModeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Remote Resource
|
||||||
|
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||||
|
# Use the mode controller for a remote because the input controller
|
||||||
|
# only allows a preset of names as an input.
|
||||||
|
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||||
|
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
||||||
|
for activity in activities:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
|
||||||
|
)
|
||||||
|
# Remotes with a single activity completely break Alexa discovery, add a
|
||||||
|
# fake activity to the mode controller (see issue #53832).
|
||||||
|
if len(activities) == 1:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Position Resources
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
self._resource = AlexaModeResource(
|
self._resource = AlexaModeResource(
|
||||||
|
@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
|
|||||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||||
|
|
||||||
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
|
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
|
||||||
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
# we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
|
||||||
PRESET_MODE_NA = "-"
|
PRESET_MODE_NA = "-"
|
||||||
|
|
||||||
STORAGE_ACCESS_TOKEN = "access_token"
|
STORAGE_ACCESS_TOKEN = "access_token"
|
||||||
|
@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Generator, Iterable
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from typing_extensions import Generator
|
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alarm_control_panel,
|
alarm_control_panel,
|
||||||
alert,
|
alert,
|
||||||
@ -29,6 +27,7 @@ from homeassistant.components import (
|
|||||||
lock,
|
lock,
|
||||||
media_player,
|
media_player,
|
||||||
number,
|
number,
|
||||||
|
remote,
|
||||||
scene,
|
scene,
|
||||||
script,
|
script,
|
||||||
sensor,
|
sensor,
|
||||||
@ -198,6 +197,10 @@ class DisplayCategory:
|
|||||||
# Indicates a device that prints.
|
# Indicates a device that prints.
|
||||||
PRINTER = "PRINTER"
|
PRINTER = "PRINTER"
|
||||||
|
|
||||||
|
# Indicates a decive that support stateless events,
|
||||||
|
# such as remote switches and smart buttons.
|
||||||
|
REMOTE = "REMOTE"
|
||||||
|
|
||||||
# Indicates a network router.
|
# Indicates a network router.
|
||||||
ROUTER = "ROUTER"
|
ROUTER = "ROUTER"
|
||||||
|
|
||||||
@ -647,6 +650,24 @@ class FanCapabilities(AlexaEntity):
|
|||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(remote.DOMAIN)
|
||||||
|
class RemoteCapabilities(AlexaEntity):
|
||||||
|
"""Class to represent Remote capabilities."""
|
||||||
|
|
||||||
|
def default_display_categories(self) -> list[str]:
|
||||||
|
"""Return the display categories for this entity."""
|
||||||
|
return [DisplayCategory.REMOTE]
|
||||||
|
|
||||||
|
def interfaces(self) -> Generator[AlexaCapability]:
|
||||||
|
"""Yield the supported interfaces."""
|
||||||
|
yield AlexaPowerController(self.entity)
|
||||||
|
yield AlexaModeController(
|
||||||
|
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||||
|
)
|
||||||
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
|
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
|
||||||
class HumidifierCapabilities(AlexaEntity):
|
class HumidifierCapabilities(AlexaEntity):
|
||||||
"""Class to represent Humidifier capabilities."""
|
"""Class to represent Humidifier capabilities."""
|
||||||
|
@ -52,7 +52,6 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
|||||||
"""Initialize Alexa view."""
|
"""Initialize Alexa view."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.flash_briefings = flash_briefings
|
self.flash_briefings = flash_briefings
|
||||||
template.attach(hass, self.flash_briefings)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def get(
|
def get(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user