Compare commits

..

3 Commits

Author SHA1 Message Date
Erik 643003c47a Merge colliding devices when loading the device registry 2024-06-26 16:45:59 +02:00
Erik d77c4cda0c Also merge config entries 2024-06-26 15:27:23 +02:00
Erik 8d06baf0a5 Merge devices on connection or identifier collision 2024-06-26 14:11:42 +02:00
3476 changed files with 91512 additions and 161845 deletions
-1
View File
@@ -49,7 +49,6 @@ base_platforms: &base_platforms
- homeassistant/components/tts/**
- homeassistant/components/update/**
- homeassistant/components/vacuum/**
- homeassistant/components/valve/**
- homeassistant/components/water_heater/**
- homeassistant/components/weather/**
+1740
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -74,6 +74,7 @@ If the code communicates with devices, web services, or third-party tools:
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
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.
- [ ] Untested files have been added to `.coveragerc`.
<!--
This project is very active and we have a high turnover of pull requests.
+10 -10
View File
@@ -32,7 +32,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: translations
path: translations.tar.gz
@@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: translations
@@ -190,7 +190,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.3.0
uses: docker/login-action@v3.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -256,7 +256,7 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.3.0
uses: docker/login-action@v3.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -329,14 +329,14 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.3.0
uses: docker/login-action@v3.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.3.0
uses: docker/login-action@v3.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -453,12 +453,12 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: translations
+45 -92
View File
@@ -36,7 +36,7 @@ env:
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.8"
HA_SHORT_VERSION: "2024.7"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
@@ -86,7 +86,7 @@ jobs:
tests_glob: ${{ steps.info.outputs.tests_glob }}
tests: ${{ steps.info.outputs.tests }}
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
@@ -218,7 +218,7 @@ jobs:
pre-commit:
name: Prepare pre-commit base
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
@@ -229,7 +229,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -266,7 +266,7 @@ jobs:
lint-ruff-format:
name: Check ruff-format
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- info
- pre-commit
@@ -274,7 +274,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -306,7 +306,7 @@ jobs:
lint-ruff:
name: Check ruff
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- info
- pre-commit
@@ -314,7 +314,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -345,7 +345,7 @@ jobs:
RUFF_OUTPUT_FORMAT: github
lint-other:
name: Check other linters
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- info
- pre-commit
@@ -353,7 +353,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -437,7 +437,7 @@ jobs:
base:
name: Prepare dependencies
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: info
timeout-minutes: 60
strategy:
@@ -448,7 +448,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -514,7 +514,7 @@ jobs:
hassfest:
name: Check hassfest
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
@@ -522,17 +522,11 @@ jobs:
- info
- base
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
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -552,7 +546,7 @@ jobs:
gen-requirements-all:
name: Check all requirements
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
@@ -564,7 +558,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -582,49 +576,9 @@ jobs:
. venv/bin/activate
python -m script.gen_requirements_all validate
audit-licenses:
name: Audit licenses
runs-on: ubuntu-24.04
needs:
- info
- base
if: |
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.4
with:
name: licenses
path: licenses.json
- name: Process licenses
run: |
. venv/bin/activate
python -m script.licenses
pylint:
name: Check pylint
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
timeout-minutes: 20
if: |
github.event.inputs.mypy-only != 'true'
@@ -637,7 +591,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -669,7 +623,7 @@ jobs:
pylint-tests:
name: Check pylint on tests
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
timeout-minutes: 20
if: |
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true')
@@ -682,7 +636,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -714,7 +668,7 @@ jobs:
mypy:
name: Check mypy
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
github.event.inputs.pylint-only != 'true'
|| github.event.inputs.mypy-only == 'true'
@@ -726,7 +680,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -775,7 +729,7 @@ jobs:
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
prepare-pytest-full:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
@@ -800,7 +754,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -818,14 +772,14 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest_buckets
path: pytest_buckets.txt
overwrite: true
pytest-full:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
@@ -863,7 +817,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -882,7 +836,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: pytest_buckets
- name: Compile English translations
@@ -904,7 +858,6 @@ jobs:
cov_params+=(--cov-report=xml)
fi
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
python3 -b -X dev -m pytest \
-qq \
--timeout=9 \
@@ -918,14 +871,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -937,7 +890,7 @@ jobs:
./script/check_dirty
pytest-mariadb:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
services:
mariadb:
image: ${{ matrix.mariadb-group }}
@@ -982,7 +935,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1043,7 +996,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1051,7 +1004,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1107,7 +1060,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1169,7 +1122,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1177,7 +1130,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1190,7 +1143,7 @@ jobs:
coverage-full:
name: Upload test coverage to Codecov (full suite)
if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- info
- pytest-full
@@ -1201,7 +1154,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1214,7 +1167,7 @@ jobs:
version: v0.6.0
pytest-partial:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
@@ -1252,7 +1205,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1310,14 +1263,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -1329,7 +1282,7 @@ jobs:
coverage-partial:
name: Upload test coverage to Codecov (partial suite)
if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- info
- pytest-partial
@@ -1338,7 +1291,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
pattern: coverage-*
- name: Upload coverage to Codecov
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.25.15
uses: github/codeql-action/init@v3.25.10
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.25.15
uses: github/codeql-action/analyze@v3.25.10
with:
category: "/language:python"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
+14 -14
View File
@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.1
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -82,14 +82,14 @@ jobs:
) > .env_file
- name: Upload env_file
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: env_file
path: ./.env_file
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -101,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.3.4
uses: actions/upload-artifact@v4.3.3
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -121,17 +121,17 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_diff
- name: Build wheels
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -159,17 +159,17 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Download env_file
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.7
with:
name: requirements_all_wheels
@@ -203,7 +203,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (old cython)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -218,7 +218,7 @@ jobs:
pip: "'cython<3'"
- name: Build wheels (part 1)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -232,7 +232,7 @@ jobs:
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -246,7 +246,7 @@ jobs:
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+2 -2
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.5
rev: v0.4.9
hooks:
- id: ruff
args:
@@ -83,7 +83,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
-9
View File
@@ -21,7 +21,6 @@ homeassistant.helpers.entity_platform
homeassistant.helpers.entity_values
homeassistant.helpers.event
homeassistant.helpers.reload
homeassistant.helpers.script
homeassistant.helpers.script_variables
homeassistant.helpers.singleton
homeassistant.helpers.sun
@@ -98,7 +97,6 @@ homeassistant.components.assist_pipeline.*
homeassistant.components.asterisk_cdr.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.*
homeassistant.components.autarco.*
homeassistant.components.auth.*
homeassistant.components.automation.*
homeassistant.components.awair.*
@@ -120,7 +118,6 @@ homeassistant.components.bond.*
homeassistant.components.braviatv.*
homeassistant.components.brother.*
homeassistant.components.browser.*
homeassistant.components.bryant_evolution.*
homeassistant.components.bthome.*
homeassistant.components.button.*
homeassistant.components.calendar.*
@@ -168,7 +165,6 @@ homeassistant.components.ecowitt.*
homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elevenlabs.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
@@ -257,7 +253,6 @@ homeassistant.components.integration.*
homeassistant.components.intent.*
homeassistant.components.intent_script.*
homeassistant.components.ios.*
homeassistant.components.iotty.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.*
homeassistant.components.islamic_prayer_times.*
@@ -282,7 +277,6 @@ homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
homeassistant.components.linear_garage_door.*
homeassistant.components.linkplay.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*
homeassistant.components.local_ip.*
@@ -293,7 +287,6 @@ homeassistant.components.logger.*
homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.*
homeassistant.components.map.*
homeassistant.components.mastodon.*
@@ -377,7 +370,6 @@ homeassistant.components.rhasspy.*
homeassistant.components.ridwell.*
homeassistant.components.ring.*
homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roborock.*
homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.rpi_power.*
@@ -389,7 +381,6 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.*
homeassistant.components.select.*
homeassistant.components.sensibo.*
+13 -29
View File
@@ -6,52 +6,38 @@
"configurations": [
{
"name": "Home Assistant",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": [
"--debug",
"-c",
"config"
],
"args": ["--debug", "-c", "config"],
"preLaunchTask": "Compile English translations"
},
{
"name": "Home Assistant (skip pip)",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": [
"--debug",
"-c",
"config",
"--skip-pip"
],
"args": ["--debug", "-c", "config", "--skip-pip"],
"preLaunchTask": "Compile English translations"
},
{
"name": "Home Assistant: Changed tests",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": false,
"args": [
"--timeout=10",
"--picked"
],
"args": ["--timeout=10", "--picked"],
},
{
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Local",
"type": "debugpy",
"type": "python",
"request": "attach",
"connect": {
"port": 5678,
"host": "localhost"
},
"port": 5678,
"host": "localhost",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
@@ -63,12 +49,10 @@
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
// See https://www.home-assistant.io/integrations/debugpy/
"name": "Home Assistant: Attach Remote",
"type": "debugpy",
"type": "python",
"request": "attach",
"connect": {
"port": 5678,
"host": "homeassistant.local"
},
"port": 5678,
"host": "homeassistant.local",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
@@ -77,4 +61,4 @@
]
}
]
}
}
-1
View File
@@ -76,7 +76,6 @@
"detail": "Generate code coverage report for a given integration.",
"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",
"dependsOn": ["Compile English translations"],
"group": {
"kind": "test",
"isDefault": true
+20 -46
View File
@@ -80,6 +80,8 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari
/homeassistant/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
/tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck
@@ -155,8 +157,6 @@ build.json @home-assistant/supervisor
/tests/components/aurora_abb_powerone/ @davet2001
/homeassistant/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
/tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core
@@ -197,8 +197,7 @@ build.json @home-assistant/supervisor
/tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core
/tests/components/blueprint/ @home-assistant/core
/homeassistant/components/bluesound/ @thrawnarn @LouisChrist
/tests/components/bluesound/ @thrawnarn @LouisChrist
/homeassistant/components/bluesound/ @thrawnarn
/homeassistant/components/bluetooth/ @bdraco
/tests/components/bluetooth/ @bdraco
/homeassistant/components/bluetooth_adapters/ @bdraco
@@ -221,8 +220,6 @@ build.json @home-assistant/supervisor
/tests/components/brottsplatskartan/ @gjohansson-ST
/homeassistant/components/brunt/ @eavanvalkenburg
/tests/components/brunt/ @eavanvalkenburg
/homeassistant/components/bryant_evolution/ @danielsmyers
/tests/components/bryant_evolution/ @danielsmyers
/homeassistant/components/bsblan/ @liudger
/tests/components/bsblan/ @liudger
/homeassistant/components/bt_smarthub/ @typhoon2099
@@ -242,8 +239,6 @@ build.json @home-assistant/supervisor
/tests/components/ccm15/ @ocalvo
/homeassistant/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_mobility_express/ @fbradyirl
/homeassistant/components/cisco_webex_teams/ @fbradyirl
@@ -365,8 +360,8 @@ build.json @home-assistant/supervisor
/tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @w1ll1am23
/tests/components/econet/ @w1ll1am23
/homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/efergy/ @tkdrob
@@ -376,8 +371,6 @@ build.json @home-assistant/supervisor
/tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000
/tests/components/electric_kiwi/ @mikey0000
/homeassistant/components/elevenlabs/ @sorgfresser
/tests/components/elevenlabs/ @sorgfresser
/homeassistant/components/elgato/ @frenck
/tests/components/elgato/ @frenck
/homeassistant/components/elkm1/ @gwww @bdraco
@@ -389,7 +382,6 @@ build.json @home-assistant/supervisor
/tests/components/elvia/ @ludeeus
/homeassistant/components/emby/ @mezz64
/homeassistant/components/emoncms/ @borpin @alexandrecuer
/tests/components/emoncms/ @borpin @alexandrecuer
/homeassistant/components/emonitor/ @bdraco
/tests/components/emonitor/ @bdraco
/homeassistant/components/emulated_hue/ @bdraco @Tho85
@@ -406,8 +398,8 @@ build.json @home-assistant/supervisor
/tests/components/enigma2/ @autinerd
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
@@ -439,8 +431,6 @@ build.json @home-assistant/supervisor
/tests/components/fan/ @home-assistant/core
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
/homeassistant/components/feedreader/ @mib1185
/tests/components/feedreader/ @mib1185
/homeassistant/components/fibaro/ @rappenze
/tests/components/fibaro/ @rappenze
/homeassistant/components/file/ @fabaff
@@ -511,7 +501,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/generic_hygrostat/ @Shulyaka
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -702,8 +691,6 @@ build.json @home-assistant/supervisor
/tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iotty/ @pburgio
/tests/components/iotty/ @pburgio
/homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes
@@ -712,14 +699,10 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/iron_os/ @tr4nt0r
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco
/tests/components/isal/ @bdraco
/homeassistant/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
/tests/components/iss/ @DurgNomis-drol
/homeassistant/components/ista_ecotrend/ @tr4nt0r
@@ -754,8 +737,8 @@ build.json @home-assistant/supervisor
/tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes
/tests/components/kmtronic/ @dgomes
/homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
/tests/components/knocki/ @joostlek @jgatto1 @JakeBosh
/homeassistant/components/knocki/ @joostlek @jgatto1
/tests/components/knocki/ @joostlek @jgatto1
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
@@ -796,14 +779,10 @@ build.json @home-assistant/supervisor
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/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/litejet/ @joncar
/tests/components/litejet/ @joncar
@@ -844,16 +823,13 @@ build.json @home-assistant/supervisor
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea
/tests/components/madvr/ @iloveicedgreentea
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
/tests/components/mastodon/ @fabaff @andrew-codechimp
/homeassistant/components/mastodon/ @fabaff
/homeassistant/components/matrix/ @PaarthShah
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
/tests/components/mealie/ @joostlek @andrew-codechimp
/homeassistant/components/mealie/ @joostlek
/tests/components/mealie/ @joostlek
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue
@@ -898,6 +874,8 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco
/homeassistant/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
/tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug
@@ -1009,6 +987,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
/homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core
/homeassistant/components/oncue/ @bdraco @peterager
@@ -1053,8 +1033,8 @@ build.json @home-assistant/supervisor
/tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
@@ -1224,8 +1204,6 @@ build.json @home-assistant/supervisor
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/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
/tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx
@@ -1293,8 +1271,6 @@ build.json @home-assistant/supervisor
/tests/components/sighthound/ @robmarkcole
/homeassistant/components/signal_messenger/ @bbernhard
/tests/components/signal_messenger/ @bbernhard
/homeassistant/components/simplefin/ @scottg489 @jeeftor
/tests/components/simplefin/ @scottg489 @jeeftor
/homeassistant/components/simplepush/ @engrbm87
/tests/components/simplepush/ @engrbm87
/homeassistant/components/simplisafe/ @bachya
@@ -1446,8 +1422,6 @@ build.json @home-assistant/supervisor
/tests/components/tellduslive/ @fredrike
/homeassistant/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
/tests/components/tesla_wall_connector/ @einarhauks
/homeassistant/components/teslemetry/ @Bre77
@@ -1486,8 +1460,8 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/homeassistant/components/tplink_omada/ @MarkGodwin
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus
+1 -1
View File
@@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.2.27
RUN pip3 install uv==0.2.13
WORKDIR /usr/src
+1 -1
View File
@@ -4,7 +4,7 @@ coverage:
status:
project:
default:
target: auto
target: 90
threshold: 0.09
required:
target: auto
+9 -9
View File
@@ -363,15 +363,15 @@ class AuthManager:
local_only: bool | None = None,
) -> None:
"""Update a user."""
kwargs: dict[str, Any] = {
attr_name: value
for attr_name, value in (
("name", name),
("group_ids", group_ids),
("local_only", local_only),
)
if value is not None
}
kwargs: dict[str, Any] = {}
for attr_name, value in (
("name", name),
("group_ids", group_ids),
("local_only", local_only),
):
if value is not None:
kwargs[attr_name] = value
await self._store.async_update_user(user, **kwargs)
if is_active is not None:
+8 -12
View File
@@ -105,18 +105,14 @@ class AuthStore:
"perm_lookup": self._perm_lookup,
}
kwargs.update(
{
attr_name: value
for attr_name, value in (
("is_owner", is_owner),
("is_active", is_active),
("local_only", local_only),
("system_generated", system_generated),
)
if value is not None
}
)
for attr_name, value in (
("is_owner", is_owner),
("is_active", is_active),
("local_only", local_only),
("system_generated", system_generated),
):
if value is not None:
kwargs[attr_name] = value
new_user = models.User(**kwargs)
+10 -10
View File
@@ -55,6 +55,13 @@ class InvalidUser(HomeAssistantError):
Will not be raised when validating authentication.
"""
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
def __init__(
self,
*args: object,
@@ -70,13 +77,6 @@ class InvalidUser(HomeAssistantError):
)
class InvalidUsername(InvalidUser):
"""Raised when invalid username is specified.
Will not be raised when validating authentication.
"""
class Data:
"""Hold the user data."""
@@ -216,7 +216,7 @@ class Data:
break
if index is None:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
self.users.pop(index)
@@ -232,7 +232,7 @@ class Data:
user["password"] = self.hash_password(new_password, True).decode()
break
else:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
@callback
def _validate_new_username(self, new_username: str) -> None:
@@ -275,7 +275,7 @@ class Data:
self._async_check_for_not_normalized_usernames(self._data)
break
else:
raise InvalidUser(translation_key="user_not_found")
raise InvalidUser
async def async_save(self) -> None:
"""Save data."""
+31 -50
View File
@@ -8,7 +8,7 @@ import contextlib
from functools import partial
from itertools import chain
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
import logging.handlers
import mimetypes
from operator import contains, itemgetter
import os
@@ -88,7 +88,7 @@ from .helpers import (
)
from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info, is_official_image
from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType
from .setup import (
# _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.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
from .util.package import async_get_user_site, is_virtual_env
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop
@@ -223,10 +223,8 @@ CRITICAL_INTEGRATIONS = {
SETUP_ORDER = (
# Load logging and http deps as soon as possible
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
# Setup frontend
("frontend", FRONTEND_INTEGRATIONS),
# Setup recorder
("recorder", RECORDER_INTEGRATIONS),
# Setup frontend and recorder
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
# Start up debuggers. Start these first in case they want to wait.
("debugger", DEBUGGER_INTEGRATIONS),
)
@@ -259,12 +257,12 @@ async def async_setup_hass(
) -> core.HomeAssistant | None:
"""Set up Home Assistant."""
async def create_hass() -> core.HomeAssistant:
def create_hass() -> core.HomeAssistant:
"""Create the hass object and do basic setup."""
hass = core.HomeAssistant(runtime_config.config_dir)
loader.async_setup(hass)
await async_enable_logging(
async_enable_logging(
hass,
runtime_config.verbose,
runtime_config.log_rotate_days,
@@ -289,7 +287,7 @@ async def async_setup_hass(
async with hass.timeout.async_timeout(10):
await hass.async_stop()
hass = await create_hass()
hass = create_hass()
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
_LOGGER.warning(
@@ -328,13 +326,13 @@ async def async_setup_hass(
if config_dict is None:
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
elif not basic_setup_success:
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
_LOGGER.warning(
@@ -347,7 +345,7 @@ async def async_setup_hass(
recovery_mode = True
await stop_hass(hass)
hass = await create_hass()
hass = create_hass()
if old_logging:
hass.data[DATA_LOGGING] = old_logging
@@ -409,10 +407,6 @@ def _init_blocking_io_modules_in_executor() -> None:
# Initialize the mimetypes module to avoid blocking calls
# to the filesystem to load the mime.types file.
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:
@@ -529,7 +523,8 @@ async def async_from_config_dict(
return hass
async def async_enable_logging(
@core.callback
def async_enable_logging(
hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: int | None = None,
@@ -612,9 +607,23 @@ async def async_enable_logging(
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)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
err_handler: (
logging.handlers.RotatingFileHandler
| 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))
@@ -631,29 +640,7 @@ async def async_enable_logging(
async_activate_log_queue_handler(hass)
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):
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler):
"""RotatingFileHandler that does not check if it should roll over on every log."""
def shouldRollover(self, record: logging.LogRecord) -> bool:
@@ -908,13 +895,7 @@ async def _async_resolve_domains_to_setup(
await asyncio.gather(*resolve_dependencies_tasks)
for itg in integrations_to_process:
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:
for dep in itg.all_dependencies:
if dep in domains_to_setup:
continue
domains_to_setup.add(dep)
+1 -1
View File
@@ -1,5 +1,5 @@
{
"domain": "logitech",
"name": "Logitech",
"integrations": ["harmony", "squeezebox"]
"integrations": ["harmony", "ue_smart_radio", "squeezebox"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"domain": "tesla",
"name": "Tesla",
"integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"]
"integrations": ["powerwall", "tesla_wall_connector"]
}
+1 -1
View File
@@ -9,5 +9,5 @@
},
"iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==5.2.1"]
"requirements": ["jaraco.abode==5.1.2"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.7.0"]
"requirements": ["adguardhome==0.6.3"]
}
@@ -206,8 +206,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC Mode and State."""
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
return
return await self.async_turn_off()
if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
raise ServiceValidationError("Heat/Cool is not supported in this mode")
await self.async_update_ac(
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling",
"loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.5.3"]
"requirements": ["AEMET-OpenData==0.5.2"]
}
@@ -9,7 +9,10 @@ from typing import Final, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType
@@ -18,11 +21,6 @@ from .const import DOMAIN
_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_CO2: Final = "carbon_dioxide"
ATTR_CO: Final = "carbon_monoxide"
@@ -35,6 +33,10 @@ ATTR_PM_10: Final = "particulate_matter_10"
ATTR_PM_2_5: Final = "particulate_matter_2_5"
ATTR_SO2: Final = "sulphur_dioxide"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
SCAN_INTERVAL: Final = timedelta(seconds=30)
PROP_TO_ATTR: Final[dict[str, str]] = {
"air_quality_index": ATTR_AQI,
"carbon_dioxide": ATTR_CO2,
@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from airgradient import AirGradientClient, get_model_name
from airgradient import AirGradientClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
@@ -20,7 +20,6 @@ PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
@@ -35,7 +34,7 @@ class AirGradientData:
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airgradient from a config entry."""
client = AirGradientClient(
@@ -53,8 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
manufacturer="AirGradient",
model=get_model_name(measurement_coordinator.data.model),
model_id=measurement_coordinator.data.model,
model=measurement_coordinator.data.model,
serial_number=measurement_coordinator.data.serial_number,
sw_version=measurement_coordinator.data.firmware_version,
)
@@ -69,8 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: AirGradientConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -2,13 +2,9 @@
from typing import Any
from airgradient import (
AirGradientClient,
AirGradientError,
AirGradientParseError,
ConfigurationControl,
)
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl
from awesomeversion import AwesomeVersion
from mashumaro import MissingField
import voluptuous as vol
from homeassistant.components import zeroconf
@@ -87,10 +83,10 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
try:
current_measures = await self.client.get_current_measures()
except AirGradientParseError:
return self.async_abort(reason="invalid_version")
except AirGradientError:
errors["base"] = "cannot_connect"
except MissingField:
return self.async_abort(reason="invalid_version")
else:
await self.async_set_unique_id(current_measures.serial_number)
self._abort_if_unique_id_configured()
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Class to manage fetching AirGradient data."""
_update_interval: timedelta
config_entry: AirGradientConfigEntry
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
@@ -27,7 +28,7 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
hass,
logger=LOGGER,
name=f"AirGradient {client.host}",
update_interval=timedelta(minutes=1),
update_interval=self._update_interval,
)
self.client = client
assert self.config_entry.unique_id
@@ -46,6 +47,8 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
"""Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=1)
async def _update_data(self) -> Measures:
return await self.client.get_current_measures()
@@ -53,5 +56,7 @@ class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
"""Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=5)
async def _update_data(self) -> Config:
return await self.client.get_config()
@@ -8,34 +8,6 @@
"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": {
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
@@ -45,32 +17,6 @@
},
"pm003_count": {
"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",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.7.1"],
"requirements": ["airgradient==0.6.0"],
"zeroconf": ["_airgradient._tcp.local."]
}
@@ -88,7 +88,6 @@ LEARNING_TIME_OFFSET_OPTIONS = [
]
ABC_DAYS = [
"1",
"8",
"30",
"90",
@@ -16,7 +16,6 @@
},
"abort": {
"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."
},
"error": {
@@ -92,9 +91,8 @@
}
},
"co2_automatic_baseline_calibration": {
"name": "CO2 automatic baseline duration",
"name": "CO2 automatic baseline calibration",
"state": {
"1": "1 day",
"8": "8 days",
"30": "30 days",
"90": "90 days",
@@ -156,11 +154,6 @@
"display_brightness": {
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
}
},
"switch": {
"post_data_to_airgradient": {
"name": "Post data to Airgradient"
}
}
}
}
@@ -1,110 +0,0 @@
"""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()
@@ -156,8 +156,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
return
return await self.async_turn_off()
await self._airtouch.SetCoolingModeForAc(
self._ac_number, HA_STATE_TO_AT[hvac_mode]
)
@@ -263,8 +262,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
raise ValueError(f"Unsupported HVAC mode: {hvac_mode}")
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
return
return await self.async_turn_off()
if self.hvac_mode == HVACMode.OFF:
await self.async_turn_on()
self._unit = self._airtouch.GetGroups()[self._group_number]
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
PLATFORMS: list[Platform] = [Platform.CLIMATE]
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
@@ -121,7 +121,6 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
"""Base class for Airtouch5 Climate Entities."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
_attr_target_temperature_step = 1
_attr_name = None
_enable_turn_on_off_backwards_compatibility = False
-134
View File
@@ -1,134 +0,0 @@
"""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,12 +6,15 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class Airtouch5Entity(Entity):
"""Base class for Airtouch5 entities."""
_attr_should_poll = False
_attr_has_entity_name = True
_attr_translation_key = DOMAIN
def __init__(self, client: Airtouch5SimpleClient) -> None:
"""Initialise the Entity."""
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.2.10"]
"requirements": ["airtouch5py==0.2.8"]
}
@@ -27,11 +27,6 @@
}
}
}
},
"cover": {
"damper": {
"name": "[%key:component::cover::entity_component::damper::name%]"
}
}
}
}
@@ -82,54 +82,33 @@ async def async_setup_entry(
"""Add Airzone binary sensors from a config_entry."""
coordinator = entry.runtime_data
added_systems: set[str] = set()
added_zones: set[str] = set()
binary_sensors: list[AirzoneBinarySensor] = [
AirzoneSystemBinarySensor(
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
]
def _async_entity_listener() -> None:
"""Handle additions of binary sensors."""
binary_sensors.extend(
AirzoneZoneBinarySensor(
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
)
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()
async_add_entities(binary_sensors)
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
+10 -24
View File
@@ -102,31 +102,17 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone climate from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of climate."""
zones_data = coordinator.data.get(AZD_ZONES, {})
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()
async_add_entities(
AirzoneClimate(
coordinator,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
)
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.8.1"]
"requirements": ["aioairzone==0.7.7"]
}
+13 -26
View File
@@ -83,34 +83,21 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone select from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of select."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in 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()
async_add_entities(
AirzoneZoneSelect(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for description in ZONE_SELECT_TYPES
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
if description.key in zone_data
)
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
+15 -34
View File
@@ -85,37 +85,21 @@ async def async_setup_entry(
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of sensors."""
entities: list[AirzoneSensor] = []
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
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] = []
sensors: list[AirzoneSensor] = [
AirzoneZoneSensor(
coordinator,
description,
entry,
system_zone_id,
zone_data,
)
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
for description in ZONE_SENSOR_TYPES
if description.key in zone_data
]
if AZD_HOT_WATER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneHotWaterSensor(
coordinator,
description,
@@ -126,7 +110,7 @@ async def async_setup_entry(
)
if AZD_WEBSERVER in coordinator.data:
entities.extend(
sensors.extend(
AirzoneWebServerSensor(
coordinator,
description,
@@ -136,10 +120,7 @@ async def async_setup_entry(
if description.key in coordinator.data[AZD_WEBSERVER]
)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
async_add_entities(sensors)
class AirzoneSensor(AirzoneEntity, SensorEntity):
@@ -61,7 +61,7 @@ async def async_setup_entry(
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone Water Heater from a config_entry."""
"""Add Airzone sensors from a config_entry."""
coordinator = entry.runtime_data
if AZD_HOT_WATER in coordinator.data:
async_add_entities([AirzoneWaterHeater(coordinator, entry)])
@@ -14,7 +14,6 @@ from aioairzone_cloud.const import (
AZD_FLOOR_DEMAND,
AZD_PROBLEMS,
AZD_SYSTEMS,
AZD_THERMOSTAT_BATTERY_LOW,
AZD_WARNINGS,
AZD_ZONES,
)
@@ -89,10 +88,6 @@ ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]
key=AZD_AQ_ACTIVE,
translation_key="air_quality_active",
),
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY_LOW,
),
AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING,
key=AZD_FLOOR_DEMAND,
@@ -13,12 +13,9 @@ from aioairzone_cloud.const import (
AZD_GROUPS,
AZD_HOT_WATERS,
AZD_INSTALLATIONS,
AZD_MODEL,
AZD_NAME,
AZD_SYSTEM_ID,
AZD_SYSTEMS,
AZD_THERMOSTAT_FW,
AZD_THERMOSTAT_MODEL,
AZD_WEBSERVER,
AZD_WEBSERVERS,
AZD_ZONES,
@@ -72,7 +69,6 @@ class AirzoneAidooEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, aidoo_id)},
manufacturer=MANUFACTURER,
model=aidoo_data[AZD_MODEL],
name=aidoo_data[AZD_NAME],
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
)
@@ -115,7 +111,6 @@ class AirzoneGroupEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, group_id)},
model="Group",
manufacturer=MANUFACTURER,
name=group_data[AZD_NAME],
)
@@ -159,7 +154,6 @@ class AirzoneHotWaterEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, dhw_id)},
manufacturer=MANUFACTURER,
model="Hot Water",
name=dhw_data[AZD_NAME],
via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]),
)
@@ -201,7 +195,6 @@ class AirzoneInstallationEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, inst_id)},
manufacturer=MANUFACTURER,
model="Installation",
name=inst_data[AZD_NAME],
)
@@ -247,11 +240,9 @@ class AirzoneSystemEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_id)},
model=system_data.get(AZD_MODEL),
manufacturer=MANUFACTURER,
name=system_data[AZD_NAME],
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
sw_version=system_data.get(AZD_FIRMWARE),
)
def get_airzone_value(self, key: str) -> Any:
@@ -279,7 +270,6 @@ class AirzoneWebServerEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
identifiers={(DOMAIN, ws_id)},
model="WebServer",
manufacturer=MANUFACTURER,
name=ws_data[AZD_NAME],
sw_version=ws_data[AZD_FIRMWARE],
@@ -310,11 +300,9 @@ class AirzoneZoneEntity(AirzoneEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, zone_id)},
model=zone_data.get(AZD_THERMOSTAT_MODEL),
manufacturer=MANUFACTURER,
name=zone_data[AZD_NAME],
via_device=(DOMAIN, self.system_id),
sw_version=zone_data.get(AZD_THERMOSTAT_FW),
)
def get_airzone_value(self, key: str) -> Any:
@@ -1,15 +0,0 @@
{
"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",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.1"]
"requirements": ["aioairzone-cloud==0.5.3"]
}
@@ -10,12 +10,8 @@ from aioairzone_cloud.const import (
AZD_AQ_PM_1,
AZD_AQ_PM_2P5,
AZD_AQ_PM_10,
AZD_CPU_USAGE,
AZD_HUMIDITY,
AZD_MEMORY_FREE,
AZD_TEMP,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_COVERAGE,
AZD_WEBSERVERS,
AZD_WIFI_RSSI,
AZD_ZONES,
@@ -32,7 +28,6 @@ from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfInformation,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -57,22 +52,6 @@ AIDOO_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(
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -119,20 +98,6 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE,
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",
),
)
@@ -37,17 +37,6 @@
"auto": "Auto"
}
}
},
"sensor": {
"cpu_usage": {
"name": "CPU usage"
},
"free_memory": {
"name": "Free memory"
},
"thermostat_coverage": {
"name": "Signal percentage"
}
}
}
}
@@ -2,37 +2,93 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from genie_partner_sdk.client import AladdinConnectClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
DOMAIN = "aladdin_connect"
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
from .coordinator import AladdinConnectCoordinator
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Aladdin Connect from a config entry."""
ir.async_create_issue(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/aladdin_connect",
},
)
async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth))
await coordinator.async_setup()
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: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Unload a config entry."""
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
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:
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:
# If device_id is None an invalid device entry was found for this config entry.
# If the device_id is not in existing device ids it's a stale device entry.
# Remove config entry from this device entry in either case.
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=config_entry.entry_id
)
@@ -0,0 +1,32 @@
"""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,12 +1,9 @@
"""Application credentials platform for iotty."""
from __future__ import annotations
"""application_credentials platform the Aladdin Connect Genie integration."""
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
OAUTH2_AUTHORIZE = "https://auth.iotty.com/.auth/oauth2/login"
OAUTH2_TOKEN = "https://auth.iotty.com/.auth/oauth2/token"
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
@@ -1,11 +1,70 @@
"""Config flow for Aladdin Connect integration."""
"""Config flow for Aladdin Connect Genie."""
from homeassistant.config_entries import ConfigFlow
from collections.abc import Mapping
import logging
from typing import Any
from . import DOMAIN
import jwt
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 AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aladdin Connect."""
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
VERSION = 1
DOMAIN = DOMAIN
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__)
@@ -0,0 +1,6 @@
"""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"
@@ -0,0 +1,38 @@
"""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)
@@ -0,0 +1,84 @@
"""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")
@@ -0,0 +1,27 @@
"""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,9 +1,10 @@
{
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"codeowners": [],
"codeowners": ["@swcloudgenie"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "system",
"iot_class": "cloud_polling",
"requirements": []
"requirements": ["genie-partner-sdk==1.0.2"]
}
@@ -0,0 +1,80 @@
"""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,8 +1,29 @@
{
"issues": {
"integration_removed": {
"title": "The Aladdin Connect integration has been removed",
"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})."
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"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,10 +52,8 @@ from .const import ( # noqa: F401
_LOGGER: Final = logging.getLogger(__name__)
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)
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
CONF_DEFAULT_CODE = "default_code"
@@ -63,6 +61,8 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
{vol.Optional(ATTR_CODE): cv.string}
)
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
# mypy: disallow-any-generics
+2 -28
View File
@@ -2,10 +2,11 @@
from __future__ import annotations
from collections.abc import Generator
import logging
from typing import Any
from typing_extensions import Generator
from homeassistant.components import (
button,
climate,
@@ -18,7 +19,6 @@ from homeassistant.components import (
light,
media_player,
number,
remote,
timer,
vacuum,
valve,
@@ -439,8 +439,6 @@ class AlexaPowerController(AlexaCapability):
is_on = self.entity.state == fan.STATE_ON
elif self.entity.domain == humidifier.DOMAIN:
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:
is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN:
@@ -1438,12 +1436,6 @@ class AlexaModeController(AlexaCapability):
if mode in modes:
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
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get(
@@ -1558,24 +1550,6 @@ class AlexaModeController(AlexaCapability):
)
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
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource(
+1 -1
View File
@@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
# we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
PRESET_MODE_NA = "-"
STORAGE_ACCESS_TOKEN = "access_token"
+3 -24
View File
@@ -2,10 +2,12 @@
from __future__ import annotations
from collections.abc import Generator, Iterable
from collections.abc import Iterable
import logging
from typing import TYPE_CHECKING, Any
from typing_extensions import Generator
from homeassistant.components import (
alarm_control_panel,
alert,
@@ -27,7 +29,6 @@ from homeassistant.components import (
lock,
media_player,
number,
remote,
scene,
script,
sensor,
@@ -197,10 +198,6 @@ class DisplayCategory:
# Indicates a device that prints.
PRINTER = "PRINTER"
# Indicates a decive that support stateless events,
# such as remote switches and smart buttons.
REMOTE = "REMOTE"
# Indicates a network router.
ROUTER = "ROUTER"
@@ -650,24 +647,6 @@ class FanCapabilities(AlexaEntity):
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)
class HumidifierCapabilities(AlexaEntity):
"""Class to represent Humidifier capabilities."""
+1 -17
View File
@@ -21,7 +21,6 @@ from homeassistant.components import (
light,
media_player,
number,
remote,
timer,
vacuum,
valve,
@@ -186,8 +185,6 @@ async def async_api_turn_on(
service = fan.SERVICE_TURN_ON
elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_ON
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_ON
elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if (
@@ -237,8 +234,6 @@ async def async_api_turn_off(
service = climate.SERVICE_TURN_OFF
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_OFF
elif domain == remote.DOMAIN:
service = remote.SERVICE_TURN_OFF
elif domain == humidifier.DOMAIN:
service = humidifier.SERVICE_TURN_OFF
elif domain == vacuum.DOMAIN:
@@ -1205,17 +1200,6 @@ async def async_api_set_mode(
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg)
# Remote Activity
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = mode.split(".")[1]
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
if activity != PRESET_MODE_NA and activities and activity in activities:
service = remote.SERVICE_TURN_ON
data[remote.ATTR_ACTIVITY] = activity
else:
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg)
# Water heater operation mode
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = mode.split(".")[1]
@@ -1513,7 +1497,7 @@ async def async_api_adjust_range(
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
service = SERVICE_SET_COVER_POSITION
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
msg = f"Unable to determine {entity.entity_id} current position"
raise AlexaInvalidValueError(msg)
position = response_value = min(100, max(0, range_delta + current))
@@ -5,6 +5,5 @@
"codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/alexa",
"integration_type": "system",
"iot_class": "cloud_push"
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"],
"requirements": ["boto3==1.34.131"]
"requirements": ["boto3==1.34.51"]
}
@@ -71,18 +71,6 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
}
class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
"""Sensor to show whether demand window is active."""
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
grid = self.coordinator.data["grid"]
if "demand_window" in grid:
return grid["demand_window"] # type: ignore[no-any-return]
return None
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@@ -95,14 +83,6 @@ async def async_setup_entry(
key="price_spike",
name=f"{entry.title} - Price Spike",
)
demand_window_description = BinarySensorEntityDescription(
key="demand_window",
name=f"{entry.title} - Demand Window",
translation_key="demand_window",
)
async_add_entities(
[
AmberPriceSpikeBinarySensor(coordinator, price_spike_description),
AmberDemandWindowBinarySensor(coordinator, demand_window_description),
]
[AmberPriceSpikeBinarySensor(coordinator, price_spike_description)]
)
@@ -111,9 +111,6 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
]
result["grid"]["renewables"] = round(general[0].renewables)
result["grid"]["price_spike"] = general[0].spike_status.value
tariff_information = general[0].tariff_information
if tariff_information:
result["grid"]["demand_window"] = tariff_information.demand_window
controlled_load = [
interval for interval in current if is_controlled_load(interval)
@@ -13,14 +13,6 @@
"renewables": {
"default": "mdi:solar-power"
}
},
"binary_sensor": {
"demand_window": {
"default": "mdi:meter-electric",
"state": {
"off": "mdi:meter-electric-outline"
}
}
}
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling",
"loggers": ["amberelectric"],
"requirements": ["amberelectric==1.1.1"]
"requirements": ["amberelectric==1.1.0"]
}
+1 -4
View File
@@ -8,7 +8,6 @@ from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
import aiohttp
from aiohttp import web
from amcrest import AmcrestError
from haffmpeg.camera import CameraMjpeg
@@ -245,9 +244,7 @@ class AmcrestCam(Camera):
websession = async_get_clientsession(self.hass)
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get(
streaming_url,
auth=self._token,
timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT),
streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT
)
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.7.0"],
"requirements": ["python-homeassistant-analytics==0.6.0"],
"single_config_entry": true
}
@@ -101,7 +101,7 @@
},
"learn_sendevent": {
"name": "Learn sendevent",
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of performing this action."
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service."
}
},
"exceptions": {
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"]
"requirements": ["anova-wifi==0.12.0"]
}
@@ -16,6 +16,8 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant
import homeassistant.helpers.config_validation as cv
@@ -119,7 +121,7 @@ class KafkaManager:
state = event.data["new_state"]
if (
state is None
or state.state == ""
or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE)
or not self._entities_filter(state.entity_id)
):
return None
@@ -139,8 +141,7 @@ class KafkaManager:
async def write(self, event: Event[EventStateChangedData]) -> None:
"""Write a binary payload to Kafka."""
key = event.data["entity_id"].encode("utf-8")
payload = self._encode_event(event)
if payload:
await self._producer.send_and_wait(self._topic, payload, key)
await self._producer.send_and_wait(self._topic, payload)
@@ -68,8 +68,4 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
"""Returns true if the UPS is online."""
# Check if ONLINE bit is set in STATFLAG.
key = self.entity_description.key.upper()
# The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag"
# suffix ("0x05000008 Status Flag") in older versions.
# Here we trim the suffix if it exists to support both.
flag = self.coordinator.data[key].removesuffix(" Status Flag")
return int(flag, 16) & _VALUE_ONLINE_MASK != 0
return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0
@@ -4,6 +4,3 @@ from typing import Final
DOMAIN: Final = "apcupsd"
CONNECTION_TIMEOUT: int = 10
# Field name of last self test retrieved from apcupsd.
LASTSTEST: Final = "laststest"
+4 -18
View File
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
STATE_UNKNOWN,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LASTSTEST
from .const import DOMAIN
from .coordinator import APCUPSdCoordinator
PARALLEL_UPDATES = 0
@@ -157,8 +156,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
LASTSTEST: SensorEntityDescription(
key=LASTSTEST,
"laststest": SensorEntityDescription(
key="laststest",
translation_key="last_self_test",
),
"lastxfer": SensorEntityDescription(
@@ -418,12 +417,7 @@ async def async_setup_entry(
available_resources: set[str] = {k.lower() for k, _ in coordinator.data.items()}
entities = []
# "laststest" is a special sensor that only appears when the APC UPS daemon has done a
# periodical (or manual) self test since last daemon restart. It might not be available
# when we set up the integration, and we do not know if it would ever be available. Here we
# add it anyway and mark it as unknown initially.
for resource in available_resources | {LASTSTEST}:
for resource in available_resources:
if resource not in SENSORS:
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
continue
@@ -479,14 +473,6 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
def _update_attrs(self) -> None:
"""Update sensor attributes based on coordinator data."""
key = self.entity_description.key.upper()
# For most sensors the key will always be available for each refresh. However, some sensors
# (e.g., "laststest") will only appear after certain event occurs (e.g., a self test is
# performed) and may disappear again after certain event. So we mark the state as "unknown"
# when it becomes unknown after such events.
if key not in self.coordinator.data:
self._attr_native_value = STATE_UNKNOWN
return
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = inferred_unit
+3 -33
View File
@@ -45,7 +45,7 @@ from homeassistant.exceptions import (
TemplateError,
Unauthorized,
)
from homeassistant.helpers import config_validation as cv, recorder, template
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.json import json_dumps, json_fragment
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.typing import ConfigType
@@ -119,10 +119,7 @@ class APICoreStateView(HomeAssistantView):
to check if Home Assistant is running.
"""
hass = request.app[KEY_HASS]
migration = recorder.async_migration_in_progress(hass)
live = recorder.async_migration_is_live(hass)
recorder_state = {"migration_in_progress": migration, "migration_is_live": live}
return self.json({"state": hass.state.value, "recorder_state": recorder_state})
return self.json({"state": hass.state.value})
class APIEventStream(HomeAssistantView):
@@ -390,27 +387,6 @@ class APIDomainServicesView(HomeAssistantView):
)
context = self.context(request)
if not hass.services.has_service(domain, service):
raise HTTPBadRequest from ServiceNotFound(domain, service)
if response_requested := "return_response" in request.query:
if (
hass.services.supports_response(domain, service)
is ha.SupportsResponse.NONE
):
return self.json_message(
"Service does not support responses. Remove return_response from request.",
HTTPStatus.BAD_REQUEST,
)
elif (
hass.services.supports_response(domain, service) is ha.SupportsResponse.ONLY
):
return self.json_message(
"Service call requires responses but caller did not ask for responses. "
"Add ?return_response to query parameters.",
HTTPStatus.BAD_REQUEST,
)
changed_states: list[json_fragment] = []
@ha.callback
@@ -427,14 +403,13 @@ class APIDomainServicesView(HomeAssistantView):
try:
# shield the service call from cancellation on connection drop
response = await shield(
await shield(
hass.services.async_call(
domain,
service,
data, # type: ignore[arg-type]
blocking=True,
context=context,
return_response=response_requested,
)
)
except (vol.Invalid, ServiceNotFound) as ex:
@@ -442,11 +417,6 @@ class APIDomainServicesView(HomeAssistantView):
finally:
cancel_listen()
if response_requested:
return self.json(
{"changed_states": changed_states, "service_response": response}
)
return self.json(changed_states)
@@ -60,7 +60,6 @@ AUTH_EXCEPTIONS = (
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
@@ -17,7 +17,6 @@ from .coordinator import AprilaireCoordinator
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.HUMIDIFIER,
Platform.SELECT,
Platform.SENSOR,
]
@@ -62,7 +62,7 @@ class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(title="AprilAire", data=user_input)
return self.async_create_entry(title="Aprilaire", data=user_input)
return self.async_show_form(
step_id="user",
@@ -1,194 +0,0 @@
"""The Aprilaire humidifier component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, cast
from pyaprilaire.const import Attribute
from homeassistant.components.humidifier import (
HumidifierAction,
HumidifierDeviceClass,
HumidifierEntity,
HumidifierEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .coordinator import AprilaireCoordinator
from .entity import BaseAprilaireEntity
HUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
0: HumidifierAction.IDLE,
1: HumidifierAction.IDLE,
2: HumidifierAction.HUMIDIFYING,
3: HumidifierAction.OFF,
}
DEHUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
0: HumidifierAction.IDLE,
1: HumidifierAction.IDLE,
2: HumidifierAction.DRYING,
3: HumidifierAction.DRYING,
4: HumidifierAction.OFF,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Aprilaire humidifier devices."""
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
assert config_entry.unique_id is not None
descriptions: list[AprilaireHumidifierDescription] = []
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2):
descriptions.append(
AprilaireHumidifierDescription(
key="humidifier",
translation_key="humidifier",
device_class=HumidifierDeviceClass.HUMIDIFIER,
action_key=Attribute.HUMIDIFICATION_STATUS,
action_map=HUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
min_humidity=10,
max_humidity=50,
default_humidity=30,
set_humidity_fn=coordinator.client.set_humidification_setpoint,
)
)
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1):
descriptions.append(
AprilaireHumidifierDescription(
key="dehumidifier",
translation_key="dehumidifier",
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
action_key=Attribute.DEHUMIDIFICATION_STATUS,
action_map=DEHUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
min_humidity=40,
max_humidity=90,
default_humidity=60,
set_humidity_fn=coordinator.client.set_dehumidification_setpoint,
)
)
async_add_entities(
AprilaireHumidifierEntity(coordinator, description, config_entry.unique_id)
for description in descriptions
)
@dataclass(frozen=True, kw_only=True)
class AprilaireHumidifierDescription(HumidifierEntityDescription):
"""Class describing Aprilaire humidifier entities."""
action_key: str
action_map: dict[StateType, HumidifierAction]
current_humidity_key: str
target_humidity_key: str
min_humidity: int
max_humidity: int
default_humidity: int
set_humidity_fn: Callable[[int], Awaitable]
class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
"""Base humidity entity for Aprilaire."""
entity_description: AprilaireHumidifierDescription
last_target_humidity: int | None = None
def __init__(
self,
coordinator: AprilaireCoordinator,
description: AprilaireHumidifierDescription,
unique_id: str,
) -> None:
"""Initialize a select for an Aprilaire device."""
self.entity_description = description
super().__init__(coordinator, unique_id)
@property
def action(self) -> HumidifierAction | None:
"""Get the current action."""
action = self.coordinator.data.get(self.entity_description.action_key)
return self.entity_description.action_map.get(action, HumidifierAction.OFF)
@property
def is_on(self) -> bool:
"""Get whether the humidifier is on."""
return self.target_humidity is not None and self.target_humidity > 0
@property
def current_humidity(self) -> float | None:
"""Get the current humidity."""
return cast(
float,
self.coordinator.data.get(self.entity_description.current_humidity_key),
)
@property
def target_humidity(self) -> float | None:
"""Get the target humidity."""
target_humidity = cast(
float,
self.coordinator.data.get(self.entity_description.target_humidity_key),
)
if target_humidity is not None and target_humidity > 0:
self.last_target_humidity = int(target_humidity)
return target_humidity
@property
def min_humidity(self) -> float:
"""Return the minimum humidity."""
return self.entity_description.min_humidity
@property
def max_humidity(self) -> float:
"""Return the maximum humidity."""
return self.entity_description.max_humidity
async def async_set_humidity(self, humidity: int) -> None:
"""Set the humidity."""
await self.entity_description.set_humidity_fn(humidity)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if self.last_target_humidity is None or self.last_target_humidity == 0:
target_humidity = self.entity_description.default_humidity
else:
target_humidity = self.last_target_humidity
await self.entity_description.set_humidity_fn(target_humidity)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.entity_description.set_humidity_fn(0)
@@ -1,11 +1,11 @@
{
"domain": "aprilaire",
"name": "AprilAire",
"name": "Aprilaire",
"codeowners": ["@chamberlain2007"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aprilaire",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.7.4"]
"requirements": ["pyaprilaire==0.7.0"]
}
@@ -24,14 +24,6 @@
"name": "Thermostat"
}
},
"humidifier": {
"humidifier": {
"name": "[%key:component::humidifier::title%]"
},
"dehumidifier": {
"name": "[%key:component::humidifier::entity_component::dehumidifier::name%]"
}
},
"select": {
"air_cleaning_event": {
"name": "Air cleaning event",
@@ -7,13 +7,12 @@ from dataclasses import dataclass
from APsystemsEZ1 import APsystemsEZ1M
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
from homeassistant.const import CONF_IP_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from .const import DEFAULT_PORT
from .coordinator import ApSystemsDataCoordinator
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
@dataclass
@@ -29,11 +28,7 @@ type ApSystemsConfigEntry = ConfigEntry[ApSystemsData]
async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) -> bool:
"""Set up this integration using UI."""
api = APsystemsEZ1M(
ip_address=entry.data[CONF_IP_ADDRESS],
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
timeout=8,
)
api = APsystemsEZ1M(ip_address=entry.data[CONF_IP_ADDRESS], timeout=8)
coordinator = ApSystemsDataCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
assert entry.unique_id
@@ -7,16 +7,14 @@ from APsystemsEZ1 import APsystemsEZ1M
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import DEFAULT_PORT, DOMAIN
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Required(CONF_IP_ADDRESS): str,
}
)
@@ -34,11 +32,7 @@ class APsystemsLocalAPIFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
session = async_get_clientsession(self.hass, False)
api = APsystemsEZ1M(
ip_address=user_input[CONF_IP_ADDRESS],
port=user_input.get(CONF_PORT, DEFAULT_PORT),
session=session,
)
api = APsystemsEZ1M(user_input[CONF_IP_ADDRESS], session=session)
try:
device_info = await api.get_device_info()
except (TimeoutError, ClientConnectionError):
@@ -4,4 +4,3 @@ from logging import Logger, getLogger
LOGGER: Logger = getLogger(__package__)
DOMAIN = "apsystems"
DEFAULT_PORT = 8050
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==1.3.3"]
"requirements": ["apsystems-ez1==1.3.1"]
}
+11 -40
View File
@@ -3,11 +3,7 @@
"step": {
"user": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"port": "The integration will default to 8050, if not set, which should be suitable for most installs"
"ip_address": "[%key:common::config_flow::data::ip%]"
}
}
},
@@ -20,43 +16,18 @@
},
"entity": {
"sensor": {
"total_power": {
"name": "Total power"
},
"total_power_p1": {
"name": "Power of P1"
},
"total_power_p2": {
"name": "Power of P2"
},
"lifetime_production": {
"name": "Total lifetime production"
},
"lifetime_production_p1": {
"name": "Lifetime production of P1"
},
"lifetime_production_p2": {
"name": "Lifetime production of P2"
},
"today_production": {
"name": "Production of today"
},
"today_production_p1": {
"name": "Production of today from P1"
},
"today_production_p2": {
"name": "Production of today from P2"
}
"total_power": { "name": "Total power" },
"total_power_p1": { "name": "Power of P1" },
"total_power_p2": { "name": "Power of P2" },
"lifetime_production": { "name": "Total lifetime production" },
"lifetime_production_p1": { "name": "Lifetime production of P1" },
"lifetime_production_p2": { "name": "Lifetime production of P2" },
"today_production": { "name": "Production of today" },
"today_production_p1": { "name": "Production of today from P1" },
"today_production_p2": { "name": "Production of today from P2" }
},
"number": {
"max_output": {
"name": "Max output"
}
},
"switch": {
"inverter_status": {
"name": "Inverter status"
}
"max_output": { "name": "Max output" }
}
}
}
@@ -1,56 +0,0 @@
"""The power switch which can be toggled via the APsystems local API integration."""
from __future__ import annotations
from typing import Any
from aiohttp.client_exceptions import ClientConnectionError
from APsystemsEZ1 import Status
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ApSystemsConfigEntry, ApSystemsData
from .entity import ApSystemsEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ApSystemsConfigEntry,
add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
add_entities([ApSystemsInverterSwitch(config_entry.runtime_data)], True)
class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
"""The switch class for APSystems switches."""
_attr_device_class = SwitchDeviceClass.SWITCH
_attr_translation_key = "inverter_status"
def __init__(self, data: ApSystemsData) -> None:
"""Initialize the switch."""
super().__init__(data)
self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_inverter_status"
async def async_update(self) -> None:
"""Update switch status and availability."""
try:
status = await self._api.get_device_power_status()
except (TimeoutError, ClientConnectionError):
self._attr_available = False
else:
self._attr_available = True
self._attr_is_on = status == Status.normal
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self._api.set_device_power_status(0)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self._api.set_device_power_status(1)
@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.2.0"]
"requirements": ["aioaquacell==0.1.7"]
}
+19 -6
View File
@@ -13,17 +13,17 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DOMAIN_DATA_ENTRIES,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
type ArcamFmjConfigEntry = ConfigEntry[Client]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -31,21 +31,34 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up config entry."""
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries = hass.data[DOMAIN_DATA_ENTRIES]
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries[entry.entry_id] = client
entry.async_create_background_task(
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
hass, _run_client(hass, client, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Cleanup before removing config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id)
return unload_ok
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:
@@ -10,11 +10,18 @@ from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES
def get_entry_client(hass: HomeAssistant, entry: ConfigEntry) -> Client:
"""Retrieve client associated with a config entry."""
client: Client = hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]
return client
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -11,3 +11,5 @@ EVENT_TURN_ON = "arcam_fmj.turn_on"
DEFAULT_PORT = 50000
DEFAULT_NAME = "Arcam FMJ"
DEFAULT_SCAN_INTERVAL = 5
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"

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