mirror of
https://github.com/home-assistant/core.git
synced 2025-09-25 04:49:24 +00:00
Compare commits
5 Commits
mqtt-clien
...
integratio
Author | SHA1 | Date | |
---|---|---|---|
![]() |
903054db14 | ||
![]() |
423f4c5bcf | ||
![]() |
9cb8b5be54 | ||
![]() |
2058777db6 | ||
![]() |
0da50c3f14 |
@@ -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
.coveragerc
Normal file
1740
.coveragerc
Normal file
File diff suppressed because it is too large
Load Diff
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -74,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.
|
||||
|
12
.github/workflows/builder.yml
vendored
12
.github/workflows/builder.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
102
.github/workflows/ci.yaml
vendored
102
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,46 +576,6 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.gen_requirements_all validate
|
||||
|
||||
audit-licenses:
|
||||
name: Audit licenses
|
||||
runs-on: ubuntu-22.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-22.04
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,7 +772,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -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
|
||||
@@ -917,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
|
||||
@@ -981,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
|
||||
@@ -1042,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 }}
|
||||
@@ -1050,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 }}
|
||||
@@ -1106,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
|
||||
@@ -1168,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 }}
|
||||
@@ -1176,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 }}
|
||||
@@ -1200,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
|
||||
@@ -1251,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
|
||||
@@ -1309,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
|
||||
@@ -1337,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
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.12
|
||||
uses: github/codeql-action/init@v3.25.10
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.12
|
||||
uses: github/codeql-action/analyze@v3.25.10
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- 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 }}
|
||||
|
||||
|
28
.github/workflows/wheels.yml
vendored
28
.github/workflows/wheels.yml
vendored
@@ -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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.2
|
||||
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
|
||||
|
@@ -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.*
|
||||
@@ -289,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.*
|
||||
@@ -373,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.*
|
||||
@@ -385,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.*
|
||||
|
42
.vscode/launch.json
vendored
42
.vscode/launch.json
vendored
@@ -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
.vscode/tasks.json
vendored
1
.vscode/tasks.json
vendored
@@ -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
|
||||
|
42
CODEOWNERS
42
CODEOWNERS
@@ -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
|
||||
@@ -239,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
|
||||
@@ -362,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
|
||||
@@ -400,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
|
||||
@@ -433,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
|
||||
@@ -707,8 +703,6 @@ build.json @home-assistant/supervisor
|
||||
/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
|
||||
@@ -743,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
|
||||
@@ -785,8 +779,6 @@ 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
|
||||
@@ -831,15 +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
|
||||
/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
|
||||
@@ -884,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
|
||||
@@ -995,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
|
||||
@@ -1210,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
|
||||
@@ -1279,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
|
||||
@@ -1470,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
|
||||
|
@@ -4,7 +4,7 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
target: 90
|
||||
threshold: 0.09
|
||||
required:
|
||||
target: auto
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
@@ -257,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,
|
||||
@@ -287,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(
|
||||
@@ -326,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(
|
||||
@@ -345,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
|
||||
@@ -407,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:
|
||||
@@ -527,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,
|
||||
@@ -610,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))
|
||||
@@ -629,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:
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
@@ -36,7 +36,7 @@ CONF_ADS_VAR_OPEN = "adsvar_open"
|
||||
CONF_ADS_VAR_CLOSE = "adsvar_close"
|
||||
CONF_ADS_VAR_STOP = "adsvar_stop"
|
||||
|
||||
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||
|
@@ -9,7 +9,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
@@ -29,7 +29,7 @@ from . import (
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "ADS Light"
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
|
||||
|
@@ -4,10 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -25,7 +22,7 @@ from . import (
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "ADS sensor"
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
|
||||
|
@@ -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,
|
||||
|
@@ -20,7 +20,6 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
|
@@ -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.6.1"],
|
||||
"requirements": ["airgradient==0.6.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
@@ -79,63 +79,6 @@ LED_BAR_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
LEARNING_TIME_OFFSET_OPTIONS = [
|
||||
"12",
|
||||
"60",
|
||||
"120",
|
||||
"360",
|
||||
"720",
|
||||
]
|
||||
|
||||
ABC_DAYS = [
|
||||
"1",
|
||||
"8",
|
||||
"30",
|
||||
"90",
|
||||
"180",
|
||||
"0",
|
||||
]
|
||||
|
||||
|
||||
def _get_value(value: int, values: list[str]) -> str | None:
|
||||
str_value = str(value)
|
||||
return str_value if str_value in values else None
|
||||
|
||||
|
||||
CONTROL_ENTITIES: tuple[AirGradientSelectEntityDescription, ...] = (
|
||||
AirGradientSelectEntityDescription(
|
||||
key="nox_index_learning_time_offset",
|
||||
translation_key="nox_index_learning_time_offset",
|
||||
options=LEARNING_TIME_OFFSET_OPTIONS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda config: _get_value(
|
||||
config.nox_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
|
||||
),
|
||||
set_value_fn=lambda client, value: client.set_nox_learning_offset(int(value)),
|
||||
),
|
||||
AirGradientSelectEntityDescription(
|
||||
key="voc_index_learning_time_offset",
|
||||
translation_key="voc_index_learning_time_offset",
|
||||
options=LEARNING_TIME_OFFSET_OPTIONS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda config: _get_value(
|
||||
config.tvoc_learning_offset, LEARNING_TIME_OFFSET_OPTIONS
|
||||
),
|
||||
set_value_fn=lambda client, value: client.set_tvoc_learning_offset(int(value)),
|
||||
),
|
||||
AirGradientSelectEntityDescription(
|
||||
key="co2_automatic_baseline_calibration",
|
||||
translation_key="co2_automatic_baseline_calibration",
|
||||
options=ABC_DAYS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda config: _get_value(
|
||||
config.co2_automatic_baseline_calibration_days, ABC_DAYS
|
||||
),
|
||||
set_value_fn=lambda client,
|
||||
value: client.set_co2_automatic_baseline_calibration(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -161,10 +104,7 @@ async def async_setup_entry(
|
||||
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||
and not added_entities
|
||||
):
|
||||
entities: list[AirGradientSelect] = [
|
||||
AirGradientSelect(coordinator, description)
|
||||
for description in CONTROL_ENTITIES
|
||||
]
|
||||
entities: list[AirGradientSelect] = []
|
||||
if "I" in model:
|
||||
entities.extend(
|
||||
AirGradientSelect(coordinator, description)
|
||||
@@ -183,9 +123,7 @@ async def async_setup_entry(
|
||||
and added_entities
|
||||
):
|
||||
entity_registry = er.async_get(hass)
|
||||
for entity_description in (
|
||||
DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES + CONTROL_ENTITIES
|
||||
):
|
||||
for entity_description in DISPLAY_SELECT_TYPES + LED_BAR_ENTITIES:
|
||||
unique_id = f"{coordinator.serial_number}-{entity_description.key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
SELECT_DOMAIN, DOMAIN, unique_id
|
||||
|
@@ -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": {
|
||||
@@ -70,37 +69,6 @@
|
||||
"co2": "Carbon dioxide",
|
||||
"pm": "Particulate matter"
|
||||
}
|
||||
},
|
||||
"nox_index_learning_time_offset": {
|
||||
"name": "NOx index learning offset",
|
||||
"state": {
|
||||
"12": "12 hours",
|
||||
"60": "60 hours",
|
||||
"120": "120 hours",
|
||||
"360": "360 hours",
|
||||
"720": "720 hours"
|
||||
}
|
||||
},
|
||||
"voc_index_learning_time_offset": {
|
||||
"name": "VOC index learning offset",
|
||||
"state": {
|
||||
"12": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::12%]",
|
||||
"60": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::60%]",
|
||||
"120": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::120%]",
|
||||
"360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]",
|
||||
"720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]"
|
||||
}
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"name": "CO2 automatic baseline duration",
|
||||
"state": {
|
||||
"1": "1 day",
|
||||
"8": "8 days",
|
||||
"30": "30 days",
|
||||
"90": "90 days",
|
||||
"180": "180 days",
|
||||
"0": "[%key:common::state::off%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -130,10 +98,10 @@
|
||||
"name": "Carbon dioxide automatic baseline calibration"
|
||||
},
|
||||
"nox_learning_offset": {
|
||||
"name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]"
|
||||
"name": "NOx learning offset"
|
||||
},
|
||||
"tvoc_learning_offset": {
|
||||
"name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]"
|
||||
"name": "VOC learning offset"
|
||||
},
|
||||
"led_bar_mode": {
|
||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||
@@ -156,11 +124,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()
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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.0"]
|
||||
"requirements": ["aioairzone==0.7.7"]
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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)])
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.5.4"]
|
||||
"requirements": ["aioairzone-cloud==0.5.3"]
|
||||
}
|
||||
|
@@ -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
|
||||
)
|
||||
|
32
homeassistant/components/aladdin_connect/api.py
Normal file
32
homeassistant/components/aladdin_connect/api.py
Normal file
@@ -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"])
|
@@ -0,0 +1,14 @@
|
||||
"""application_credentials platform the Aladdin Connect Genie integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
return AuthorizationServer(
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
@@ -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__)
|
||||
|
6
homeassistant/components/aladdin_connect/const.py
Normal file
6
homeassistant/components/aladdin_connect/const.py
Normal file
@@ -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"
|
38
homeassistant/components/aladdin_connect/coordinator.py
Normal file
38
homeassistant/components/aladdin_connect/coordinator.py
Normal file
@@ -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)
|
84
homeassistant/components/aladdin_connect/cover.py
Normal file
84
homeassistant/components/aladdin_connect/cover.py
Normal file
@@ -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")
|
27
homeassistant/components/aladdin_connect/entity.py
Normal file
27
homeassistant/components/aladdin_connect/entity.py
Normal file
@@ -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"]
|
||||
}
|
||||
|
80
homeassistant/components/aladdin_connect/sensor.py
Normal file
80
homeassistant/components/aladdin_connect/sensor.py
Normal file
@@ -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,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(
|
||||
|
@@ -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"
|
||||
|
@@ -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."""
|
||||
|
@@ -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))
|
||||
|
@@ -10,10 +10,7 @@ from alpha_vantage.timeseries import TimeSeries
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -62,7 +59,7 @@ CURRENCY_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.2.0"]
|
||||
"requirements": ["aioaquacell==0.1.7"]
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = (
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_KEYS)]
|
||||
|
@@ -10,7 +10,7 @@ import sharp_aquos_rc
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
@@ -37,7 +37,7 @@ DEFAULT_PASSWORD = "password"
|
||||
DEFAULT_TIMEOUT = 0.5
|
||||
DEFAULT_RETRIES = 2
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
@@ -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"
|
||||
|
@@ -19,6 +19,7 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -26,7 +27,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ArcamFmjConfigEntry
|
||||
from .config_flow import get_entry_client
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVENT_TURN_ON,
|
||||
@@ -40,12 +41,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ArcamFmjConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the configuration entry."""
|
||||
|
||||
client = config_entry.runtime_data
|
||||
client = get_entry_client(hass, config_entry)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
|
@@ -9,10 +9,7 @@ import logging
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_VARIABLES,
|
||||
CONF_NAME,
|
||||
@@ -44,7 +41,7 @@ PIN_VARIABLE_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["asyncarve==0.1.1"]
|
||||
"requirements": ["asyncarve==0.0.9"]
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import array
|
||||
import asyncio
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncGenerator, AsyncIterable, Callable
|
||||
from collections.abc import AsyncIterable, Callable, Iterable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
@@ -16,6 +16,7 @@ import time
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
||||
import wave
|
||||
|
||||
from typing_extensions import AsyncGenerator
|
||||
import voluptuous as vol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -115,13 +116,10 @@ AUDIO_PROCESSOR_SAMPLES: Final = 160 # 10 ms @ 16 Khz
|
||||
AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples
|
||||
|
||||
|
||||
@callback
|
||||
def _async_resolve_default_pipeline_settings(
|
||||
async def _async_resolve_default_pipeline_settings(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
conversation_engine_id: str | None = None,
|
||||
stt_engine_id: str | None = None,
|
||||
tts_engine_id: str | None = None,
|
||||
stt_engine_id: str | None,
|
||||
tts_engine_id: str | None,
|
||||
pipeline_name: str,
|
||||
) -> dict[str, str | None]:
|
||||
"""Resolve settings for a default pipeline.
|
||||
@@ -139,13 +137,12 @@ def _async_resolve_default_pipeline_settings(
|
||||
wake_word_entity = None
|
||||
wake_word_id = None
|
||||
|
||||
if conversation_engine_id is None:
|
||||
conversation_engine_id = conversation.HOME_ASSISTANT_AGENT
|
||||
|
||||
# Find a matching language supported by the Home Assistant conversation agent
|
||||
conversation_languages = language_util.matches(
|
||||
hass.config.language,
|
||||
conversation.async_get_conversation_languages(hass, conversation_engine_id),
|
||||
await conversation.async_get_conversation_languages(
|
||||
hass, conversation.HOME_ASSISTANT_AGENT
|
||||
),
|
||||
country=hass.config.country,
|
||||
)
|
||||
if conversation_languages:
|
||||
@@ -204,7 +201,7 @@ def _async_resolve_default_pipeline_settings(
|
||||
tts_engine_id = None
|
||||
|
||||
return {
|
||||
"conversation_engine": conversation_engine_id,
|
||||
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
|
||||
"conversation_language": conversation_language,
|
||||
"language": hass.config.language,
|
||||
"name": pipeline_name,
|
||||
@@ -226,8 +223,8 @@ async def _async_create_default_pipeline(
|
||||
The default pipeline will use the homeassistant conversation agent and the
|
||||
default stt / tts engines.
|
||||
"""
|
||||
pipeline_settings = _async_resolve_default_pipeline_settings(
|
||||
hass, pipeline_name="Home Assistant"
|
||||
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||
hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant"
|
||||
)
|
||||
return await pipeline_store.async_create_item(pipeline_settings)
|
||||
|
||||
@@ -245,11 +242,8 @@ async def async_create_default_pipeline(
|
||||
"""
|
||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||
pipeline_store = pipeline_data.pipeline_store
|
||||
pipeline_settings = _async_resolve_default_pipeline_settings(
|
||||
hass,
|
||||
stt_engine_id=stt_engine_id,
|
||||
tts_engine_id=tts_engine_id,
|
||||
pipeline_name=pipeline_name,
|
||||
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||
hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name
|
||||
)
|
||||
if (
|
||||
pipeline_settings["stt_engine"] != stt_engine_id
|
||||
@@ -259,22 +253,6 @@ async def async_create_default_pipeline(
|
||||
return await pipeline_store.async_create_item(pipeline_settings)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_pipeline_from_conversation_entity(
|
||||
hass: HomeAssistant, entity_id: str
|
||||
) -> Pipeline:
|
||||
"""Get a pipeline by conversation entity ID."""
|
||||
entity = hass.states.get(entity_id)
|
||||
settings = _async_resolve_default_pipeline_settings(
|
||||
hass,
|
||||
pipeline_name=entity.name if entity else entity_id,
|
||||
conversation_engine_id=entity_id,
|
||||
)
|
||||
settings["id"] = entity_id
|
||||
|
||||
return Pipeline.from_json(settings)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> Pipeline:
|
||||
"""Get a pipeline by id or the preferred pipeline."""
|
||||
@@ -284,9 +262,6 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
|
||||
# A pipeline was not specified, use the preferred one
|
||||
pipeline_id = pipeline_data.pipeline_store.async_get_preferred_item()
|
||||
|
||||
if pipeline_id.startswith("conversation."):
|
||||
return _async_get_pipeline_from_conversation_entity(hass, pipeline_id)
|
||||
|
||||
pipeline = pipeline_data.pipeline_store.data.get(pipeline_id)
|
||||
|
||||
# If invalid pipeline ID was specified
|
||||
@@ -299,11 +274,11 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pipelines(hass: HomeAssistant) -> list[Pipeline]:
|
||||
def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]:
|
||||
"""Get all pipelines."""
|
||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||
|
||||
return list(pipeline_data.pipeline_store.data.values())
|
||||
return pipeline_data.pipeline_store.data.values()
|
||||
|
||||
|
||||
async def async_update_pipeline(
|
||||
@@ -329,25 +304,21 @@ async def async_update_pipeline(
|
||||
updates.pop("id")
|
||||
# Refactor this once we bump to Python 3.12
|
||||
# and have https://peps.python.org/pep-0692/
|
||||
updates.update(
|
||||
{
|
||||
key: val
|
||||
for key, val in (
|
||||
("conversation_engine", conversation_engine),
|
||||
("conversation_language", conversation_language),
|
||||
("language", language),
|
||||
("name", name),
|
||||
("stt_engine", stt_engine),
|
||||
("stt_language", stt_language),
|
||||
("tts_engine", tts_engine),
|
||||
("tts_language", tts_language),
|
||||
("tts_voice", tts_voice),
|
||||
("wake_word_entity", wake_word_entity),
|
||||
("wake_word_id", wake_word_id),
|
||||
)
|
||||
if val is not UNDEFINED
|
||||
}
|
||||
)
|
||||
for key, val in (
|
||||
("conversation_engine", conversation_engine),
|
||||
("conversation_language", conversation_language),
|
||||
("language", language),
|
||||
("name", name),
|
||||
("stt_engine", stt_engine),
|
||||
("stt_language", stt_language),
|
||||
("tts_engine", tts_engine),
|
||||
("tts_language", tts_language),
|
||||
("tts_voice", tts_voice),
|
||||
("wake_word_entity", wake_word_entity),
|
||||
("wake_word_id", wake_word_id),
|
||||
):
|
||||
if val is not UNDEFINED:
|
||||
updates[key] = val
|
||||
|
||||
await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates)
|
||||
|
||||
@@ -884,15 +855,6 @@ class PipelineRun:
|
||||
stream: AsyncIterable[ProcessedAudioChunk],
|
||||
) -> str:
|
||||
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
|
||||
# Create a background task to prepare the conversation agent
|
||||
if self.end_stage >= PipelineStage.INTENT:
|
||||
self.hass.async_create_background_task(
|
||||
conversation.async_prepare_agent(
|
||||
self.hass, self.intent_agent, self.language
|
||||
),
|
||||
f"prepare conversation agent {self.intent_agent}",
|
||||
)
|
||||
|
||||
if isinstance(self.stt_provider, stt.Provider):
|
||||
engine = self.stt_provider.name
|
||||
else:
|
||||
@@ -995,6 +957,8 @@ class PipelineRun:
|
||||
"""Prepare recognizing an intent."""
|
||||
agent_info = conversation.async_get_agent_info(
|
||||
self.hass,
|
||||
# If no conversation engine is set, use the Home Assistant agent
|
||||
# (the conversation integration default is currently the last one set)
|
||||
self.pipeline.conversation_engine or conversation.HOME_ASSISTANT_AGENT,
|
||||
)
|
||||
|
||||
@@ -1689,12 +1653,6 @@ class PipelineStorageCollectionWebsocket(
|
||||
if item_id is None:
|
||||
item_id = self.storage_collection.async_get_preferred_item()
|
||||
|
||||
if item_id.startswith("conversation.") and hass.states.get(item_id):
|
||||
connection.send_result(
|
||||
msg["id"], _async_get_pipeline_from_conversation_entity(hass, item_id)
|
||||
)
|
||||
return
|
||||
|
||||
if item_id not in self.storage_collection.data:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
@@ -1713,7 +1671,7 @@ class PipelineStorageCollectionWebsocket(
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"pipelines": async_get_pipelines(hass),
|
||||
"pipelines": self.storage_collection.async_items(),
|
||||
"preferred_pipeline": self.storage_collection.async_get_preferred_item(),
|
||||
},
|
||||
)
|
||||
|
@@ -5,12 +5,13 @@ import asyncio
|
||||
# Suppressing disable=deprecated-module is needed for Python 3.11
|
||||
import audioop # pylint: disable=deprecated-module
|
||||
import base64
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Final
|
||||
|
||||
from typing_extensions import AsyncGenerator
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, stt, tts, websocket_api
|
||||
@@ -378,8 +379,8 @@ def websocket_get_run(
|
||||
vol.Required("type"): "assist_pipeline/language/list",
|
||||
}
|
||||
)
|
||||
@callback
|
||||
def websocket_list_languages(
|
||||
@websocket_api.async_response
|
||||
async def websocket_list_languages(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.connection.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -389,7 +390,7 @@ def websocket_list_languages(
|
||||
This will return a list of languages which are supported by at least one stt, tts
|
||||
and conversation engine respectively.
|
||||
"""
|
||||
conv_language_tags = conversation.async_get_conversation_languages(hass)
|
||||
conv_language_tags = await conversation.async_get_conversation_languages(hass)
|
||||
stt_language_tags = stt.async_get_speech_to_text_languages(hass)
|
||||
tts_language_tags = tts.async_get_text_to_speech_languages(hass)
|
||||
pipeline_languages: set[str] | None = None
|
||||
|
@@ -9,7 +9,7 @@ from pyatome.client import AtomeClient, PyAtomeError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
@@ -49,7 +49,7 @@ WEEKLY_TYPE = "week"
|
||||
MONTHLY_TYPE = "month"
|
||||
YEARLY_TYPE = "year"
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from path import Path
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail, LockDoorStatus
|
||||
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
|
||||
@@ -26,25 +26,67 @@ from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .entity import AugustDescriptionEntity
|
||||
from .util import (
|
||||
retrieve_ding_activity,
|
||||
retrieve_doorbell_motion_activity,
|
||||
retrieve_online_state,
|
||||
retrieve_time_based_activity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
|
||||
TIME_TO_RECHECK_DETECTION = timedelta(
|
||||
seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
# to consider is available or we will not report motion or dings
|
||||
if isinstance(detail, DoorbellDetail):
|
||||
return detail.is_online or detail.is_standby
|
||||
return detail.bridge_is_online
|
||||
|
||||
|
||||
def _retrieve_time_based_state(
|
||||
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
stream = data.activity_stream
|
||||
if latest := stream.get_latest_device_activity(detail.device_id, activities):
|
||||
return _activity_time_based_state(latest)
|
||||
return False
|
||||
|
||||
|
||||
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
|
||||
|
||||
|
||||
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool:
|
||||
stream = data.activity_stream
|
||||
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
|
||||
if latest is None or (
|
||||
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
|
||||
):
|
||||
return False
|
||||
return _activity_time_based_state(latest)
|
||||
|
||||
|
||||
def _activity_time_based_state(latest: Activity) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
return start <= _native_datetime() <= end
|
||||
|
||||
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AugustDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes August binary_sensor entity."""
|
||||
|
||||
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
|
||||
value_fn: Callable[[AugustData, DoorbellDetail], bool]
|
||||
is_time_based: bool
|
||||
|
||||
|
||||
@@ -57,14 +99,14 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
value_fn=retrieve_doorbell_motion_activity,
|
||||
value_fn=partial(_retrieve_time_based_state, {ActivityType.DOORBELL_MOTION}),
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="image capture",
|
||||
translation_key="image_capture",
|
||||
value_fn=partial(
|
||||
retrieve_time_based_activity, {ActivityType.DOORBELL_IMAGE_CAPTURE}
|
||||
_retrieve_time_based_state, {ActivityType.DOORBELL_IMAGE_CAPTURE}
|
||||
),
|
||||
is_time_based=True,
|
||||
),
|
||||
@@ -72,7 +114,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=retrieve_online_state,
|
||||
value_fn=_retrieve_online_state,
|
||||
is_time_based=False,
|
||||
),
|
||||
)
|
||||
@@ -82,7 +124,7 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] =
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=retrieve_ding_activity,
|
||||
value_fn=_retrieve_ding_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
)
|
||||
@@ -147,12 +189,10 @@ class AugustDoorbellBinarySensor(AugustDescriptionEntity, BinarySensorEntity):
|
||||
def _update_from_data(self) -> None:
|
||||
"""Get the latest state of the sensor."""
|
||||
self._cancel_any_pending_updates()
|
||||
self._attr_is_on = bool(
|
||||
self.entity_description.value_fn(self._data, self._detail)
|
||||
)
|
||||
self._attr_is_on = self.entity_description.value_fn(self._data, self._detail)
|
||||
|
||||
if self.entity_description.is_time_based:
|
||||
self._attr_available = retrieve_online_state(self._data, self._detail)
|
||||
self._attr_available = _retrieve_online_state(self._data, self._detail)
|
||||
self._schedule_update_to_recheck_turn_off_sensor()
|
||||
else:
|
||||
self._attr_available = True
|
||||
|
@@ -42,7 +42,6 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CAMERA,
|
||||
Platform.EVENT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
@@ -1,104 +0,0 @@
|
||||
"""Support for august events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from yalexs.activity import Activity
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .entity import AugustDescriptionEntity
|
||||
from .util import (
|
||||
retrieve_ding_activity,
|
||||
retrieve_doorbell_motion_activity,
|
||||
retrieve_online_state,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class AugustEventEntityDescription(EventEntityDescription):
|
||||
"""Describe august event entities."""
|
||||
|
||||
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
|
||||
|
||||
|
||||
TYPES_VIDEO_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
|
||||
AugustEventEntityDescription(
|
||||
key="motion",
|
||||
translation_key="motion",
|
||||
device_class=EventDeviceClass.MOTION,
|
||||
event_types=["motion"],
|
||||
value_fn=retrieve_doorbell_motion_activity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
TYPES_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
|
||||
AugustEventEntityDescription(
|
||||
key="doorbell",
|
||||
translation_key="doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=["ring"],
|
||||
value_fn=retrieve_ding_activity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AugustConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the august event platform."""
|
||||
data = config_entry.runtime_data
|
||||
entities: list[AugustEventEntity] = []
|
||||
|
||||
for lock in data.locks:
|
||||
detail = data.get_device_detail(lock.device_id)
|
||||
if detail.doorbell:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, lock, description)
|
||||
for description in TYPES_DOORBELL
|
||||
)
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, doorbell, description)
|
||||
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AugustEventEntity(AugustDescriptionEntity, EventEntity):
|
||||
"""An august event entity."""
|
||||
|
||||
entity_description: AugustEventEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_last_activity: Activity | None = None
|
||||
|
||||
@callback
|
||||
def _update_from_data(self) -> None:
|
||||
"""Update from data."""
|
||||
self._attr_available = retrieve_online_state(self._data, self._detail)
|
||||
current_activity = self.entity_description.value_fn(self._data, self._detail)
|
||||
if not current_activity or current_activity == self._last_activity:
|
||||
return
|
||||
self._last_activity = current_activity
|
||||
event_types = self.entity_description.event_types
|
||||
if TYPE_CHECKING:
|
||||
assert event_types is not None
|
||||
self._trigger_event(event_type=event_types[0])
|
||||
self.async_write_ha_state()
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
@@ -58,26 +58,6 @@
|
||||
"operator": {
|
||||
"name": "Operator"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"doorbell": {
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"ring": "Ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"motion": "Motion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,24 +1,12 @@
|
||||
"""August util functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import socket
|
||||
|
||||
import aiohttp
|
||||
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail
|
||||
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from . import AugustData
|
||||
|
||||
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSession:
|
||||
@@ -34,60 +22,3 @@ def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSess
|
||||
# we can allow IPv6 again
|
||||
#
|
||||
return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET)
|
||||
|
||||
|
||||
def retrieve_time_based_activity(
|
||||
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
stream = data.activity_stream
|
||||
if latest := stream.get_latest_device_activity(detail.device_id, activities):
|
||||
return _activity_time_based(latest)
|
||||
return False
|
||||
|
||||
|
||||
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
|
||||
|
||||
|
||||
def retrieve_ding_activity(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> Activity | None:
|
||||
"""Get the ring/ding state."""
|
||||
stream = data.activity_stream
|
||||
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
|
||||
if latest is None or (
|
||||
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
|
||||
):
|
||||
return None
|
||||
return _activity_time_based(latest)
|
||||
|
||||
|
||||
retrieve_doorbell_motion_activity = partial(
|
||||
retrieve_time_based_activity, {ActivityType.DOORBELL_MOTION}
|
||||
)
|
||||
|
||||
|
||||
def _activity_time_based(latest: Activity) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
if start <= _native_datetime() <= end:
|
||||
return latest
|
||||
return None
|
||||
|
||||
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
# to consider is available or we will not report motion or dings
|
||||
if isinstance(detail, DoorbellDetail):
|
||||
return detail.is_online or detail.is_standby
|
||||
return detail.bridge_is_online
|
||||
|
@@ -1,49 +0,0 @@
|
||||
"""The Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from autarco import Autarco
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AutarcoDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Set up Autarco from a config entry."""
|
||||
client = Autarco(
|
||||
email=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
account_sites = await client.get_account()
|
||||
|
||||
coordinators: list[AutarcoDataUpdateCoordinator] = [
|
||||
AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites
|
||||
]
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators
|
||||
]
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinators
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
@@ -1,57 +0,0 @@
|
||||
"""Config flow for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Autarco."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
|
||||
client = Autarco(
|
||||
email=user_input[CONF_EMAIL],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await client.get_account()
|
||||
except AutarcoAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AutarcoConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL],
|
||||
data={
|
||||
CONF_EMAIL: user_input[CONF_EMAIL],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=DATA_SCHEMA,
|
||||
)
|
@@ -1,11 +0,0 @@
|
||||
"""Constants for the Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "autarco"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
@@ -1,49 +0,0 @@
|
||||
"""Coordinator for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from autarco import AccountSite, Autarco, Inverter, Solar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
|
||||
class AutarcoData(NamedTuple):
|
||||
"""Class for defining data in dict."""
|
||||
|
||||
solar: Solar
|
||||
inverters: dict[str, Inverter]
|
||||
|
||||
|
||||
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
|
||||
"""Class to manage fetching Autarco data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: Autarco,
|
||||
site: AccountSite,
|
||||
) -> None:
|
||||
"""Initialize global Autarco data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.site = site
|
||||
|
||||
async def _async_update_data(self) -> AutarcoData:
|
||||
"""Fetch data from Autarco API."""
|
||||
return AutarcoData(
|
||||
solar=await self.client.get_solar(self.site.public_key),
|
||||
inverters=await self.client.get_inverters(self.site.public_key),
|
||||
)
|
@@ -1,43 +0,0 @@
|
||||
"""Support for the Autarco diagnostics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AutarcoConfigEntry, AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AutarcoConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
autarco_data: list[AutarcoDataUpdateCoordinator] = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"sites_data": [
|
||||
{
|
||||
"id": coordinator.site.site_id,
|
||||
"name": coordinator.site.system_name,
|
||||
"health": coordinator.site.health,
|
||||
"solar": {
|
||||
"power_production": coordinator.data.solar.power_production,
|
||||
"energy_production_today": coordinator.data.solar.energy_production_today,
|
||||
"energy_production_month": coordinator.data.solar.energy_production_month,
|
||||
"energy_production_total": coordinator.data.solar.energy_production_total,
|
||||
},
|
||||
"inverters": [
|
||||
{
|
||||
"serial_number": inverter.serial_number,
|
||||
"out_ac_power": inverter.out_ac_power,
|
||||
"out_ac_energy_total": inverter.out_ac_energy_total,
|
||||
"grid_turned_off": inverter.grid_turned_off,
|
||||
"health": inverter.health,
|
||||
}
|
||||
for inverter in coordinator.data.inverters.values()
|
||||
],
|
||||
}
|
||||
for coordinator in autarco_data
|
||||
],
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "autarco",
|
||||
"name": "Autarco",
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["autarco==2.0.0"]
|
||||
}
|
@@ -1,189 +0,0 @@
|
||||
"""Support for Autarco sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from autarco import Inverter, Solar
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AutarcoConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco sensor entity."""
|
||||
|
||||
value_fn: Callable[[Solar], StateType]
|
||||
|
||||
|
||||
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="power_production",
|
||||
translation_key="power_production",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda solar: solar.power_production,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_today",
|
||||
translation_key="energy_production_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
value_fn=lambda solar: solar.energy_production_today,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_month",
|
||||
translation_key="energy_production_month",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
value_fn=lambda solar: solar.energy_production_month,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_total",
|
||||
translation_key="energy_production_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda solar: solar.energy_production_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutarcoInverterSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco inverter sensor entity."""
|
||||
|
||||
value_fn: Callable[[Inverter], StateType]
|
||||
|
||||
|
||||
SENSORS_INVERTER: tuple[AutarcoInverterSensorEntityDescription, ...] = (
|
||||
AutarcoInverterSensorEntityDescription(
|
||||
key="out_ac_power",
|
||||
translation_key="out_ac_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda inverter: inverter.out_ac_power,
|
||||
),
|
||||
AutarcoInverterSensorEntityDescription(
|
||||
key="out_ac_energy_total",
|
||||
translation_key="out_ac_energy_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda inverter: inverter.out_ac_energy_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AutarcoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Autarco sensors based on a config entry."""
|
||||
entities: list[SensorEntity] = []
|
||||
for coordinator in entry.runtime_data:
|
||||
entities.extend(
|
||||
AutarcoSolarSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
)
|
||||
for description in SENSORS_SOLAR
|
||||
)
|
||||
entities.extend(
|
||||
AutarcoInverterSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
serial_number=inverter,
|
||||
)
|
||||
for description in SENSORS_INVERTER
|
||||
for inverter in coordinator.data.inverters
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AutarcoSolarSensorEntity(
|
||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an Autarco solar sensor."""
|
||||
|
||||
entity_description: AutarcoSolarSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: AutarcoDataUpdateCoordinator,
|
||||
description: AutarcoSolarSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Autarco sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.site.site_id}_solar_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.site.site_id}_solar")},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="Autarco",
|
||||
name="Solar",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data.solar)
|
||||
|
||||
|
||||
class AutarcoInverterSensorEntity(
|
||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an Autarco inverter sensor."""
|
||||
|
||||
entity_description: AutarcoInverterSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: AutarcoDataUpdateCoordinator,
|
||||
description: AutarcoInverterSensorEntityDescription,
|
||||
serial_number: str,
|
||||
) -> None:
|
||||
"""Initialize Autarco sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._serial_number = serial_number
|
||||
self._attr_unique_id = f"{serial_number}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"Inverter {serial_number}",
|
||||
manufacturer="Autarco",
|
||||
model="Inverter",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data.inverters[self._serial_number]
|
||||
)
|
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Connect to your Autarco account to get information about your solar panels.",
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "The email address of your Autarco account.",
|
||||
"password": "The password of your Autarco account."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"power_production": {
|
||||
"name": "Power production"
|
||||
},
|
||||
"energy_production_today": {
|
||||
"name": "Energy production today"
|
||||
},
|
||||
"energy_production_month": {
|
||||
"name": "Energy production month"
|
||||
},
|
||||
"energy_production_total": {
|
||||
"name": "Energy production total"
|
||||
},
|
||||
"out_ac_power": {
|
||||
"name": "Power AC output"
|
||||
},
|
||||
"out_ac_energy_total": {
|
||||
"name": "Energy AC output total"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -215,7 +215,7 @@ def _prepare_result_json(
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
@@ -156,7 +156,7 @@ def _prepare_result_json(
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user