mirror of
https://github.com/home-assistant/core.git
synced 2025-11-06 17:40:11 +00:00
Compare commits
208 Commits
2024.10.0b
...
light-stat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be3b18cebf | ||
|
|
1dff94e29c | ||
|
|
5b886d48cf | ||
|
|
ddf7f08567 | ||
|
|
d62da432d0 | ||
|
|
0999f61079 | ||
|
|
c1c6e6d50e | ||
|
|
7f88813ba1 | ||
|
|
3f2292dae4 | ||
|
|
dcb6c9a133 | ||
|
|
edcb4eca22 | ||
|
|
de6ca56504 | ||
|
|
fdd9fca5b3 | ||
|
|
10805805fe | ||
|
|
05288dad51 | ||
|
|
053ff33ef9 | ||
|
|
c97f1baa2b | ||
|
|
0f4c50e83c | ||
|
|
0a99c1c633 | ||
|
|
d6ae47a0de | ||
|
|
b258e6464d | ||
|
|
86a95013b6 | ||
|
|
636cba5d6b | ||
|
|
74931071de | ||
|
|
c92169cb20 | ||
|
|
927813ab3b | ||
|
|
060268747c | ||
|
|
47c953209d | ||
|
|
16df3eb995 | ||
|
|
454fb30759 | ||
|
|
4e157c2999 | ||
|
|
d96fd518e7 | ||
|
|
07fa1fa771 | ||
|
|
404b3fcd03 | ||
|
|
e1db5f3cac | ||
|
|
4bb768f39c | ||
|
|
730012edfd | ||
|
|
352987db7e | ||
|
|
92a6f231a9 | ||
|
|
a44bf164e5 | ||
|
|
5cc8cfb209 | ||
|
|
f99b7d8b78 | ||
|
|
e8fd97e355 | ||
|
|
5e64caa225 | ||
|
|
0672e1a1ea | ||
|
|
2deab9e0c2 | ||
|
|
ebe4ed99b5 | ||
|
|
dce51b02c8 | ||
|
|
70d4ee93f5 | ||
|
|
34a4372190 | ||
|
|
301543d3d0 | ||
|
|
c3c2bc51c5 | ||
|
|
40f808e9be | ||
|
|
3caf6c0e31 | ||
|
|
36a0c1b514 | ||
|
|
e128751e64 | ||
|
|
fae1efc237 | ||
|
|
dec03d4d25 | ||
|
|
064bbab3f5 | ||
|
|
f03e81544e | ||
|
|
4c8027aefa | ||
|
|
dbecd7a99c | ||
|
|
b035649c75 | ||
|
|
97ab595e20 | ||
|
|
20d4031ed4 | ||
|
|
812be801ce | ||
|
|
672a7ca740 | ||
|
|
0b3d69aa8e | ||
|
|
e87542e091 | ||
|
|
e9bbf773d6 | ||
|
|
68e8c968a8 | ||
|
|
a3f12329b3 | ||
|
|
f5ef213842 | ||
|
|
b573e5a2b3 | ||
|
|
17c3e7b238 | ||
|
|
9921a67a05 | ||
|
|
ad09197c00 | ||
|
|
be11d1cabf | ||
|
|
5399e2b648 | ||
|
|
a8d72cfdcf | ||
|
|
fbeee11fd7 | ||
|
|
545dae2e7f | ||
|
|
86891351f6 | ||
|
|
ddfe790995 | ||
|
|
85a9a8eca1 | ||
|
|
52c358e120 | ||
|
|
f516e538a8 | ||
|
|
4c28c1f556 | ||
|
|
b996bd3e65 | ||
|
|
6c1167df4a | ||
|
|
1044345587 | ||
|
|
d9eb419ecc | ||
|
|
8a266aac34 | ||
|
|
f6ac5dab74 | ||
|
|
d34ba16a30 | ||
|
|
5638e937b0 | ||
|
|
2ff88e7baf | ||
|
|
2e1732fadf | ||
|
|
57e041171b | ||
|
|
317b73ffaf | ||
|
|
39a9634a5c | ||
|
|
22ebc654a7 | ||
|
|
20c3b9b6f9 | ||
|
|
7588d83c6c | ||
|
|
efbb5bf9af | ||
|
|
dac69fafb8 | ||
|
|
8d98085873 | ||
|
|
8950e817e0 | ||
|
|
6fb1b53039 | ||
|
|
c81a4f8633 | ||
|
|
4599d1650b | ||
|
|
4edc3872ce | ||
|
|
8999e9f116 | ||
|
|
c5b4892596 | ||
|
|
46812777e2 | ||
|
|
616c0ebaa4 | ||
|
|
33d0343089 | ||
|
|
e6af8f63f3 | ||
|
|
cda62a4ff3 | ||
|
|
f359d619cb | ||
|
|
495faf5033 | ||
|
|
cba2daf314 | ||
|
|
2d68f9a986 | ||
|
|
7fde2e2fe0 | ||
|
|
bd4f3b0553 | ||
|
|
6373347d65 | ||
|
|
b458b204f0 | ||
|
|
dd3a3f821c | ||
|
|
60e3a1fc5f | ||
|
|
706a5680e1 | ||
|
|
cad87f51a3 | ||
|
|
66ab90b518 | ||
|
|
8bdd81ff24 | ||
|
|
f64e542879 | ||
|
|
7c58476af9 | ||
|
|
1f3b06a9bd | ||
|
|
e1314b6cda | ||
|
|
a6b629c392 | ||
|
|
f9f51e2381 | ||
|
|
ee75cba008 | ||
|
|
c768f03f71 | ||
|
|
20a57d6381 | ||
|
|
308f25fe4c | ||
|
|
9f2ba6bc2c | ||
|
|
b3b5d9602a | ||
|
|
2d16732972 | ||
|
|
94efd3e230 | ||
|
|
59a690f214 | ||
|
|
6f70a52880 | ||
|
|
85ebe5e01a | ||
|
|
a3ec4db9cc | ||
|
|
ffa6b5fcb2 | ||
|
|
b78a1f7b61 | ||
|
|
1d49c5056c | ||
|
|
83ebd601a9 | ||
|
|
a972e295ea | ||
|
|
7a0b4fc62c | ||
|
|
d78fcd2a29 | ||
|
|
2b2f5c9353 | ||
|
|
fcbb9dd8d8 | ||
|
|
b34f3ad5c5 | ||
|
|
8bdd909351 | ||
|
|
d7fe7f35ad | ||
|
|
1ebcc34e66 | ||
|
|
40e83dd9e0 | ||
|
|
18fd00d0c2 | ||
|
|
9ec26a9be5 | ||
|
|
3c0be47d3c | ||
|
|
26b5dab12b | ||
|
|
75ae6a8087 | ||
|
|
fb0e102d74 | ||
|
|
d777ec3267 | ||
|
|
cff9e9abab | ||
|
|
0b19831a7a | ||
|
|
27f3715780 | ||
|
|
76858f0534 | ||
|
|
bcfdfe93f9 | ||
|
|
7c6cc16ef1 | ||
|
|
471c68f653 | ||
|
|
ae102f1318 | ||
|
|
2a0ad20188 | ||
|
|
9db5b481be | ||
|
|
185d00c86c | ||
|
|
e72ec07683 | ||
|
|
6e12726b11 | ||
|
|
6c539cd2d8 | ||
|
|
77642b9e3d | ||
|
|
86dc7111cb | ||
|
|
8d428acbbb | ||
|
|
1c13851858 | ||
|
|
a75ebc27c4 | ||
|
|
45f92dd981 | ||
|
|
5a6ce86476 | ||
|
|
7afad1dde9 | ||
|
|
b766d91f49 | ||
|
|
22dac266c4 | ||
|
|
5fb9537d6d | ||
|
|
c1b24e6ba2 | ||
|
|
cf803507d6 | ||
|
|
16e5271cac | ||
|
|
d5ad35630f | ||
|
|
1395baef01 | ||
|
|
90dcb02429 | ||
|
|
4f0211cdd8 | ||
|
|
a1e6d4b693 | ||
|
|
77db88ad28 | ||
|
|
c6a1b9fc39 | ||
|
|
9afd270111 |
18
.github/workflows/builder.yml
vendored
18
.github/workflows/builder.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.6.0
|
||||
@@ -451,7 +451,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -499,7 +499,7 @@ jobs:
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 10
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2024.10"
|
||||
HA_SHORT_VERSION: "2024.11"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate_python_cache_key
|
||||
run: |
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
@@ -317,7 +317,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
@@ -357,7 +357,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
- script/hassfest/docker/Dockerfile
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -550,7 +550,7 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -583,7 +583,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -617,7 +617,7 @@ jobs:
|
||||
&& needs.info.outputs.requirements == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -645,7 +645,7 @@ jobs:
|
||||
- name: Process licenses
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -m script.licenses
|
||||
python -m script.licenses licenses.json
|
||||
|
||||
pylint:
|
||||
name: Check pylint
|
||||
@@ -660,7 +660,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -707,7 +707,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -752,7 +752,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -815,7 +815,11 @@ jobs:
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
name: Split tests for full run
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
name: Split tests for full run Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Install additional OS dependencies
|
||||
run: |
|
||||
@@ -827,12 +831,12 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
@@ -891,7 +895,7 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -1011,7 +1015,7 @@ jobs:
|
||||
libturbojpeg \
|
||||
libmariadb-dev-compat
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -1137,7 +1141,7 @@ jobs:
|
||||
libturbojpeg \
|
||||
postgresql-server-dev-14
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -1232,7 +1236,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
@@ -1283,7 +1287,7 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
@@ -1370,7 +1374,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.26.9
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
|
||||
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
@@ -205,11 +205,9 @@ jobs:
|
||||
# Some dependencies still require 'cython<3'
|
||||
# and don't yet use isolated build environments.
|
||||
# Build these first.
|
||||
# grpcio: https://github.com/grpc/grpc/issues/33918
|
||||
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
||||
|
||||
touch requirements_old-cython.txt
|
||||
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt
|
||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
||||
|
||||
- name: Build wheels (old cython)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.6
|
||||
rev: v0.6.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@@ -83,7 +83,7 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements\.txt)$
|
||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
||||
- id: hassfest-metadata
|
||||
name: hassfest-metadata
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||
|
||||
@@ -7,8 +7,6 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||
|
||||
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Featured integrations
|
||||
@@ -22,9 +20,14 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
|
||||
|ohf-logo|
|
||||
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://www.home-assistant.io/join-chat/
|
||||
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
||||
:target: https://demo.home-assistant.io
|
||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
||||
:target: https://home-assistant.io/integrations/
|
||||
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
|
||||
:alt: Home Assistant - A project from the Open Home Foundation
|
||||
:target: https://www.openhomefoundation.org/
|
||||
|
||||
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
from jaraco.abode.client import Client as Abode
|
||||
import jaraco.abode.config
|
||||
from jaraco.abode.exceptions import (
|
||||
AuthenticationException as AbodeAuthenticationException,
|
||||
Exception as AbodeException,
|
||||
@@ -93,6 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
polling = entry.data[CONF_POLLING]
|
||||
|
||||
# Configure abode library to use config directory for storing data
|
||||
jaraco.abode.config.paths.override(user_data=Path(hass.config.path("Abode")))
|
||||
|
||||
# For previous config entries where unique_id is None
|
||||
if entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==6.2.0"]
|
||||
"requirements": ["jaraco.abode==6.2.1"]
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ from typing import TYPE_CHECKING
|
||||
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AirGradientConfigEntry
|
||||
@@ -29,6 +30,7 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
config_entry: AirGradientConfigEntry
|
||||
_current_version: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||
"""Initialize coordinator."""
|
||||
@@ -42,11 +44,27 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
assert self.config_entry.unique_id
|
||||
self.serial_number = self.config_entry.unique_id
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
|
||||
async def _async_update_data(self) -> AirGradientData:
|
||||
try:
|
||||
measures = await self.client.get_current_measures()
|
||||
config = await self.client.get_config()
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
else:
|
||||
return AirGradientData(measures, config)
|
||||
if measures.firmware_version != self._current_version:
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.serial_number)}
|
||||
)
|
||||
assert device_entry
|
||||
device_registry.async_update_device(
|
||||
device_entry.id,
|
||||
sw_version=measures.firmware_version,
|
||||
)
|
||||
self._current_version = measures.firmware_version
|
||||
return AirGradientData(measures, config)
|
||||
|
||||
18
homeassistant/components/airgradient/diagnostics.py
Normal file
18
homeassistant/components/airgradient/diagnostics.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Diagnostics support for Airgradient."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirGradientConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return asdict(entry.runtime_data.data)
|
||||
@@ -10,12 +10,15 @@ from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .analytics import Analytics
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
@@ -52,7 +55,7 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.data[DOMAIN] = analytics
|
||||
hass.data[DATA_COMPONENT] = analytics
|
||||
return True
|
||||
|
||||
|
||||
@@ -65,7 +68,7 @@ def websocket_analytics(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return analytics preferences."""
|
||||
analytics: Analytics = hass.data[DOMAIN]
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
||||
@@ -87,7 +90,7 @@ async def websocket_analytics_preferences(
|
||||
) -> None:
|
||||
"""Update analytics preferences."""
|
||||
preferences = msg[ATTR_PREFERENCES]
|
||||
analytics: Analytics = hass.data[DOMAIN]
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
|
||||
await analytics.save_preferences(preferences)
|
||||
await analytics.send_analytics()
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -15,8 +14,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -26,7 +24,9 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Android IP Webcam from a config entry."""
|
||||
websession = async_get_clientsession(hass)
|
||||
cam = PyDroidIPCam(
|
||||
@@ -40,16 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -7,12 +7,11 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MOTION_ACTIVE
|
||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .const import MOTION_ACTIVE
|
||||
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||
from .entity import AndroidIPCamBaseEntity
|
||||
|
||||
BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
|
||||
@@ -24,16 +23,12 @@ BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidIPCamConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the IP Webcam sensors from config entry."""
|
||||
|
||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
async_add_entities([IPWebcamBinarySensor(coordinator)])
|
||||
async_add_entities([IPWebcamBinarySensor(config_entry.runtime_data)])
|
||||
|
||||
|
||||
class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -15,21 +14,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidIPCamConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the IP Webcam camera from config entry."""
|
||||
filter_urllib3_logging()
|
||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
async_add_entities([IPWebcamCamera(coordinator)])
|
||||
async_add_entities([IPWebcamCamera(config_entry.runtime_data)])
|
||||
|
||||
|
||||
class IPWebcamCamera(MjpegCamera):
|
||||
|
||||
@@ -15,19 +15,22 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AndroidIPCamConfigEntry = ConfigEntry[AndroidIPCamDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator class for the Android IP Webcam."""
|
||||
|
||||
config_entry: AndroidIPCamConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidIPCamConfigEntry,
|
||||
cam: PyDroidIPCam,
|
||||
) -> None:
|
||||
"""Initialize the Android IP Webcam."""
|
||||
self.hass = hass
|
||||
self.config_entry: ConfigEntry = config_entry
|
||||
self.cam = cam
|
||||
super().__init__(
|
||||
self.hass,
|
||||
|
||||
@@ -13,14 +13,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
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 AndroidIPCamDataUpdateCoordinator
|
||||
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||
from .entity import AndroidIPCamBaseEntity
|
||||
|
||||
|
||||
@@ -120,19 +118,21 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidIPCamConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the IP Webcam sensors from config entry."""
|
||||
|
||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinator = config_entry.runtime_data
|
||||
sensor_types = [
|
||||
sensor
|
||||
for sensor in SENSOR_TYPES
|
||||
if sensor.key
|
||||
in [*coordinator.cam.enabled_sensors, "audio_connections", "video_connections"]
|
||||
in [
|
||||
*coordinator.cam.enabled_sensors,
|
||||
"audio_connections",
|
||||
"video_connections",
|
||||
]
|
||||
]
|
||||
async_add_entities(
|
||||
IPWebcamSensor(coordinator, description) for description in sensor_types
|
||||
|
||||
@@ -9,13 +9,11 @@ from typing import Any
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||
from .entity import AndroidIPCamBaseEntity
|
||||
|
||||
|
||||
@@ -113,14 +111,12 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidIPCamConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the IP Webcam switches from config entry."""
|
||||
|
||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinator = config_entry.runtime_data
|
||||
switch_types = [
|
||||
switch
|
||||
for switch in SWITCH_TYPES
|
||||
|
||||
@@ -53,7 +53,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate the Apache Kafka integration."""
|
||||
conf = config[DOMAIN]
|
||||
|
||||
kafka = hass.data[DOMAIN] = KafkaManager(
|
||||
kafka = KafkaManager(
|
||||
hass,
|
||||
conf[CONF_IP_ADDRESS],
|
||||
conf[CONF_PORT],
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Mapping
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
|
||||
from pyatv import exceptions, pair, scan
|
||||
from pyatv.const import DeviceModel, PairingRequirement, Protocol
|
||||
@@ -98,8 +98,11 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
scan_filter: str | None = None
|
||||
all_identifiers: set[str]
|
||||
atv: BaseConfig | None = None
|
||||
atv_identifiers: list[str] | None = None
|
||||
_host: str # host in zeroconf discovery info, should not be accessed by other flows
|
||||
host: str | None = None # set by _async_aggregate_discoveries, for other flows
|
||||
protocol: Protocol | None = None
|
||||
pairing: PairingHandler | None = None
|
||||
protocols_to_pair: deque[Protocol] | None = None
|
||||
@@ -157,7 +160,6 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"type": "Apple TV",
|
||||
}
|
||||
self.scan_filter = self.unique_id
|
||||
self.context["identifier"] = self.unique_id
|
||||
return await self.async_step_restore_device()
|
||||
|
||||
async def async_step_restore_device(
|
||||
@@ -192,7 +194,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.device_identifier, raise_on_progress=False
|
||||
)
|
||||
assert self.atv
|
||||
self.context["all_identifiers"] = self.atv.all_identifiers
|
||||
self.all_identifiers = set(self.atv.all_identifiers)
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -207,7 +209,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle device found via zeroconf."""
|
||||
if discovery_info.ip_address.version == 6:
|
||||
return self.async_abort(reason="ipv6_not_supported")
|
||||
host = discovery_info.host
|
||||
self._host = host = discovery_info.host
|
||||
service_type = discovery_info.type[:-1] # Remove leading .
|
||||
name = discovery_info.name.replace(f".{service_type}.", "")
|
||||
properties = discovery_info.properties
|
||||
@@ -255,7 +257,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# as two separate flows.
|
||||
#
|
||||
# To solve this, all identifiers are stored as
|
||||
# "all_identifiers" in the flow context. When a new service is discovered, the
|
||||
# "all_identifiers" in the flow. When a new service is discovered, the
|
||||
# code below will check these identifiers for all active flows and abort if a
|
||||
# match is found. Before aborting, the original flow is updated with any
|
||||
# potentially new identifiers. In the example above, when service C is
|
||||
@@ -277,32 +279,32 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_check_and_update_in_progress(host, unique_id)
|
||||
# Host must only be set AFTER checking and updating in progress
|
||||
# flows or we will have a race condition where no flows move forward.
|
||||
self.context[CONF_ADDRESS] = host
|
||||
self.host = host
|
||||
|
||||
@callback
|
||||
def _async_check_and_update_in_progress(self, host: str, unique_id: str) -> None:
|
||||
"""Check for in-progress flows and update them with identifiers if needed."""
|
||||
for flow in self._async_in_progress(include_uninitialized=True):
|
||||
context = flow["context"]
|
||||
if (
|
||||
context.get("source") != SOURCE_ZEROCONF
|
||||
or context.get(CONF_ADDRESS) != host
|
||||
):
|
||||
continue
|
||||
if (
|
||||
"all_identifiers" in context
|
||||
and unique_id not in context["all_identifiers"]
|
||||
):
|
||||
# Add potentially new identifiers from this device to the existing flow
|
||||
context["all_identifiers"].append(unique_id)
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
raise AbortFlow("already_in_progress")
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
if (
|
||||
other_flow.context.get("source") != SOURCE_ZEROCONF
|
||||
or other_flow.host != self._host
|
||||
):
|
||||
return False
|
||||
if self.unique_id is not None:
|
||||
# Add potentially new identifiers from this device to the existing flow
|
||||
other_flow.all_identifiers.add(self.unique_id)
|
||||
return True
|
||||
|
||||
async def async_found_zeroconf_device(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle device found after Zeroconf discovery."""
|
||||
assert self.atv
|
||||
self.context["all_identifiers"] = self.atv.all_identifiers
|
||||
self.all_identifiers = set(self.atv.all_identifiers)
|
||||
# Also abort if an integration with this identifier already exists
|
||||
await self.async_set_unique_id(self.device_identifier)
|
||||
# but be sure to update the address if its changed so the scanner
|
||||
@@ -310,7 +312,6 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_ADDRESS: str(self.atv.address)}
|
||||
)
|
||||
self.context["identifier"] = self.unique_id
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_find_device_wrapper(
|
||||
@@ -390,7 +391,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
assert self.atv
|
||||
if user_input is not None:
|
||||
expected_identifier_count = len(self.context["all_identifiers"])
|
||||
expected_identifier_count = len(self.all_identifiers)
|
||||
# If number of services found during device scan mismatch number of
|
||||
# identifiers collected during Zeroconf discovery, then trigger a new scan
|
||||
# with hopes of finding all services.
|
||||
|
||||
@@ -36,6 +36,7 @@ from homeassistant.loader import (
|
||||
async_get_integration,
|
||||
)
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
__all__ = ["ClientCredential", "AuthorizationServer", "async_import_client_credential"]
|
||||
|
||||
@@ -45,7 +46,7 @@ DOMAIN = "application_credentials"
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
DATA_STORAGE = "storage"
|
||||
DATA_COMPONENT: HassKey[ApplicationCredentialsStorageCollection] = HassKey(DOMAIN)
|
||||
CONF_AUTH_DOMAIN = "auth_domain"
|
||||
DEFAULT_IMPORT_NAME = "Import from configuration.yaml"
|
||||
|
||||
@@ -150,7 +151,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
id_manager,
|
||||
)
|
||||
await storage_collection.async_load()
|
||||
hass.data[DOMAIN][DATA_STORAGE] = storage_collection
|
||||
hass.data[DATA_COMPONENT] = storage_collection
|
||||
|
||||
collection.DictStorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
@@ -175,7 +176,6 @@ async def async_import_client_credential(
|
||||
"""Import an existing credential from configuration.yaml."""
|
||||
if DOMAIN not in hass.data:
|
||||
raise ValueError("Integration 'application_credentials' not setup")
|
||||
storage_collection = hass.data[DOMAIN][DATA_STORAGE]
|
||||
item = {
|
||||
CONF_DOMAIN: domain,
|
||||
CONF_CLIENT_ID: credential.client_id,
|
||||
@@ -183,7 +183,7 @@ async def async_import_client_credential(
|
||||
CONF_AUTH_DOMAIN: auth_domain if auth_domain else domain,
|
||||
}
|
||||
item[CONF_NAME] = credential.name if credential.name else DEFAULT_IMPORT_NAME
|
||||
await storage_collection.async_import_item(item)
|
||||
await hass.data[DATA_COMPONENT].async_import_item(item)
|
||||
|
||||
|
||||
class AuthImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation):
|
||||
@@ -222,8 +222,7 @@ async def _async_provide_implementation(
|
||||
if not platform:
|
||||
return []
|
||||
|
||||
storage_collection = hass.data[DOMAIN][DATA_STORAGE]
|
||||
credentials = storage_collection.async_client_credentials(domain)
|
||||
credentials = hass.data[DATA_COMPONENT].async_client_credentials(domain)
|
||||
if hasattr(platform, "async_get_auth_implementation"):
|
||||
return [
|
||||
await platform.async_get_auth_implementation(hass, auth_domain, credential)
|
||||
@@ -246,8 +245,7 @@ async def _async_config_entry_app_credentials(
|
||||
):
|
||||
return None
|
||||
|
||||
storage_collection = hass.data[DOMAIN][DATA_STORAGE]
|
||||
for item in storage_collection.async_items():
|
||||
for item in hass.data[DATA_COMPONENT].async_items():
|
||||
item_id = item[CONF_ID]
|
||||
if (
|
||||
item[CONF_DOMAIN] == config_entry.domain
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apprise",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["apprise"],
|
||||
"requirements": ["apprise==1.8.0"]
|
||||
"requirements": ["apprise==1.9.0"]
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import logging
|
||||
|
||||
from pyaprilaire.const import Attribute
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AprilaireCoordinator
|
||||
from .coordinator import AprilaireConfigEntry, AprilaireCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
@@ -25,7 +23,7 @@ PLATFORMS: list[Platform] = [
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AprilaireConfigEntry) -> bool:
|
||||
"""Set up a config entry for Aprilaire."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
@@ -34,15 +32,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = AprilaireCoordinator(hass, entry.unique_id, host, port)
|
||||
await coordinator.start_listen()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = coordinator
|
||||
|
||||
async def ready_callback(ready: bool):
|
||||
async def ready_callback(ready: bool) -> None:
|
||||
if ready:
|
||||
mac_address = format_mac(coordinator.data[Attribute.MAC_ADDRESS])
|
||||
|
||||
if mac_address != entry.unique_id:
|
||||
raise ConfigEntryAuthFailed("Invalid MAC address")
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.async_on_unload(coordinator.stop_listen)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def _async_close(_: Event) -> None:
|
||||
@@ -63,12 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AprilaireConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN].pop(entry.unique_id)
|
||||
coordinator.stop_listen()
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -16,19 +16,17 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PRECISION_HALVES, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FAN_CIRCULATE,
|
||||
PRESET_PERMANENT_HOLD,
|
||||
PRESET_TEMPORARY_HOLD,
|
||||
PRESET_VACATION,
|
||||
)
|
||||
from .coordinator import AprilaireCoordinator
|
||||
from .coordinator import AprilaireConfigEntry
|
||||
from .entity import BaseAprilaireEntity
|
||||
|
||||
HVAC_MODE_MAP = {
|
||||
@@ -64,14 +62,14 @@ FAN_MODE_MAP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AprilaireConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add climates for passed config_entry in HA."""
|
||||
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
|
||||
|
||||
async_add_entities([AprilaireClimate(coordinator, config_entry.unique_id)])
|
||||
async_add_entities(
|
||||
[AprilaireClimate(config_entry.runtime_data, config_entry.unique_id)]
|
||||
)
|
||||
|
||||
|
||||
class AprilaireClimate(BaseAprilaireEntity, ClimateEntity):
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
import pyaprilaire.client
|
||||
from pyaprilaire.const import MODELS, Attribute, FunctionalDomain
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -22,6 +23,8 @@ WAIT_TIMEOUT = 30
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AprilaireConfigEntry = ConfigEntry[AprilaireCoordinator]
|
||||
|
||||
|
||||
class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
|
||||
"""Coordinator for interacting with the thermostat."""
|
||||
@@ -112,7 +115,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
|
||||
self.client.stop_listen()
|
||||
|
||||
async def wait_for_ready(
|
||||
self, ready_callback: Callable[[bool], Awaitable[bool]]
|
||||
self, ready_callback: Callable[[bool], Awaitable[None]]
|
||||
) -> bool:
|
||||
"""Wait for the client to be ready."""
|
||||
|
||||
|
||||
@@ -14,13 +14,11 @@ from homeassistant.components.humidifier import (
|
||||
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 .coordinator import AprilaireConfigEntry, AprilaireCoordinator
|
||||
from .entity import BaseAprilaireEntity
|
||||
|
||||
HUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
|
||||
@@ -41,12 +39,12 @@ DEHUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AprilaireConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aprilaire humidifier devices."""
|
||||
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
assert config_entry.unique_id is not None
|
||||
|
||||
|
||||
@@ -9,12 +9,10 @@ from typing import cast
|
||||
from pyaprilaire.const import Attribute
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AprilaireCoordinator
|
||||
from .coordinator import AprilaireConfigEntry, AprilaireCoordinator
|
||||
from .entity import BaseAprilaireEntity
|
||||
|
||||
AIR_CLEANING_EVENT_MAP = {0: "off", 3: "event_clean", 4: "allergies"}
|
||||
@@ -25,12 +23,12 @@ FRESH_AIR_MODE_MAP = {0: "off", 1: "automatic"}
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AprilaireConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aprilaire select devices."""
|
||||
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
assert config_entry.unique_id is not None
|
||||
|
||||
|
||||
@@ -13,14 +13,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
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 .coordinator import AprilaireConfigEntry, AprilaireCoordinator
|
||||
from .entity import BaseAprilaireEntity
|
||||
|
||||
DEHUMIDIFICATION_STATUS_MAP: dict[StateType, str] = {
|
||||
@@ -76,12 +74,12 @@ def get_entities(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AprilaireConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aprilaire sensor devices."""
|
||||
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
assert config_entry.unique_id is not None
|
||||
|
||||
|
||||
@@ -2,33 +2,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ArveCoordinator
|
||||
from .coordinator import ArveConfigEntry, ArveCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
|
||||
"""Set up Arve from a config entry."""
|
||||
|
||||
coordinator = ArveCoordinator(hass)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -21,11 +21,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
type ArveConfigEntry = ConfigEntry[ArveCoordinator]
|
||||
|
||||
|
||||
class ArveCoordinator(DataUpdateCoordinator[ArveSensProData]):
|
||||
"""Arve coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: ArveConfigEntry
|
||||
devices: ArveDevices
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
@@ -21,8 +20,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ArveCoordinator
|
||||
from .coordinator import ArveConfigEntry
|
||||
from .entity import ArveDeviceEntity
|
||||
|
||||
|
||||
@@ -85,10 +83,10 @@ SENSORS: tuple[ArveDeviceEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: ArveConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Arve device based on a config entry."""
|
||||
coordinator: ArveCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
ArveDevice(coordinator, description, sn)
|
||||
|
||||
@@ -6,20 +6,18 @@ import logging
|
||||
|
||||
from aioaseko import Aseko, AsekoNotLoggedIn
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
from .coordinator import AsekoConfigEntry, AsekoDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AsekoConfigEntry) -> bool:
|
||||
"""Set up Aseko Pool Live from a config entry."""
|
||||
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||
|
||||
@@ -30,19 +28,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AsekoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AsekoConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
|
||||
@@ -11,12 +11,10 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
from .coordinator import AsekoConfigEntry
|
||||
from .entity import AsekoEntity
|
||||
|
||||
|
||||
@@ -38,11 +36,11 @@ BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AsekoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live binary sensors."""
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
AsekoBinarySensorEntity(unit, coordinator, description)
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
|
||||
from aioaseko import Aseko, Unit
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
@@ -14,6 +15,8 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AsekoConfigEntry = ConfigEntry[AsekoDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
|
||||
"""Class to manage fetching Aseko unit data from single endpoint."""
|
||||
|
||||
@@ -13,14 +13,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
|
||||
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 AsekoDataUpdateCoordinator
|
||||
from .coordinator import AsekoConfigEntry
|
||||
from .entity import AsekoEntity
|
||||
|
||||
|
||||
@@ -80,11 +78,11 @@ SENSORS: list[AsekoSensorEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AsekoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live sensors."""
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
AsekoSensorEntity(unit, coordinator, description)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"select": {
|
||||
"pipeline": {
|
||||
"name": "Assist pipeline",
|
||||
"name": "Assistant",
|
||||
"state": {
|
||||
"preferred": "Preferred"
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AssistSatelliteState(StrEnum):
|
||||
"""Valid states of an Assist satellite entity."""
|
||||
|
||||
LISTENING_WAKE_WORD = "listening_wake_word"
|
||||
"""Device is streaming audio for wake word detection to Home Assistant."""
|
||||
IDLE = "idle"
|
||||
"""Device is waiting for user input, such as a wake word or a button press."""
|
||||
|
||||
LISTENING_COMMAND = "listening_command"
|
||||
LISTENING = "listening"
|
||||
"""Device is streaming audio with the voice command to Home Assistant."""
|
||||
|
||||
PROCESSING = "processing"
|
||||
@@ -117,7 +117,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
_attr_tts_options: dict[str, Any] | None = None
|
||||
_pipeline_task: asyncio.Task | None = None
|
||||
|
||||
__assist_satellite_state = AssistSatelliteState.LISTENING_WAKE_WORD
|
||||
__assist_satellite_state = AssistSatelliteState.IDLE
|
||||
|
||||
@final
|
||||
@property
|
||||
@@ -242,7 +242,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
)
|
||||
finally:
|
||||
self._is_announcing = False
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
async def async_announce(self, announcement: AssistSatelliteAnnouncement) -> None:
|
||||
"""Announce media on the satellite.
|
||||
@@ -363,9 +363,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
def _internal_on_pipeline_event(self, event: PipelineEvent) -> None:
|
||||
"""Set state based on pipeline stage."""
|
||||
if event.type is PipelineEventType.WAKE_WORD_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
elif event.type is PipelineEventType.STT_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING_COMMAND)
|
||||
self._set_state(AssistSatelliteState.LISTENING)
|
||||
elif event.type is PipelineEventType.INTENT_START:
|
||||
self._set_state(AssistSatelliteState.PROCESSING)
|
||||
elif event.type is PipelineEventType.INTENT_END:
|
||||
@@ -379,7 +379,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self._set_state(AssistSatelliteState.RESPONDING)
|
||||
elif event.type is PipelineEventType.RUN_END:
|
||||
if not self._run_has_tts:
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
self.on_pipeline_event(event)
|
||||
|
||||
@@ -392,7 +392,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
@callback
|
||||
def tts_response_finished(self) -> None:
|
||||
"""Tell entity that the text-to-speech response has finished playing."""
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
@callback
|
||||
def _resolve_pipeline(self) -> str | None:
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"_": {
|
||||
"name": "Assist satellite",
|
||||
"state": {
|
||||
"listening_wake_word": "Wake word",
|
||||
"listening_command": "Voice command",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"listening": "Listening",
|
||||
"responding": "Responding",
|
||||
"processing": "Processing"
|
||||
}
|
||||
|
||||
@@ -1,61 +1,29 @@
|
||||
"""The ATAG Integration."""
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyatag import AtagException, AtagOne
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .coordinator import AtagConfigEntry, AtagDataUpdateCoordinator
|
||||
|
||||
DOMAIN = "atag"
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AtagConfigEntry) -> bool:
|
||||
"""Set up Atag integration from a config entry."""
|
||||
|
||||
async def _async_update_data():
|
||||
"""Update data via library."""
|
||||
async with timeout(20):
|
||||
try:
|
||||
await atag.update()
|
||||
except AtagException as err:
|
||||
raise UpdateFailed(err) from err
|
||||
return atag
|
||||
|
||||
atag = AtagOne(
|
||||
session=async_get_clientsession(hass), **entry.data, device=entry.unique_id
|
||||
)
|
||||
coordinator = DataUpdateCoordinator[AtagOne](
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN.title(),
|
||||
update_method=_async_update_data,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
|
||||
coordinator = AtagDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
if entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(entry, unique_id=atag.id)
|
||||
hass.config_entries.async_update_entry(entry, unique_id=coordinator.atag.id)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AtagConfigEntry) -> bool:
|
||||
"""Unload Atag config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -12,13 +12,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from . import DOMAIN
|
||||
from .coordinator import AtagConfigEntry, AtagDataUpdateCoordinator
|
||||
from .entity import AtagEntity
|
||||
|
||||
PRESET_MAP = {
|
||||
@@ -33,11 +32,10 @@ HVAC_MODES = [HVACMode.AUTO, HVACMode.HEAT]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant, entry: AtagConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Load a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([AtagThermostat(coordinator, Platform.CLIMATE)])
|
||||
async_add_entities([AtagThermostat(entry.runtime_data, "climate")])
|
||||
|
||||
|
||||
class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
@@ -50,49 +48,49 @@ class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
)
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator, atag_id):
|
||||
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None:
|
||||
"""Initialize an Atag climate device."""
|
||||
super().__init__(coordinator, atag_id)
|
||||
self._attr_temperature_unit = coordinator.data.climate.temp_unit
|
||||
self._attr_temperature_unit = coordinator.atag.climate.temp_unit
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
return try_parse_enum(HVACMode, self.coordinator.data.climate.hvac_mode)
|
||||
return try_parse_enum(HVACMode, self.coordinator.atag.climate.hvac_mode)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running hvac operation."""
|
||||
is_active = self.coordinator.data.climate.status
|
||||
is_active = self.coordinator.atag.climate.status
|
||||
return HVACAction.HEATING if is_active else HVACAction.IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
return self.coordinator.data.climate.temperature
|
||||
return self.coordinator.atag.climate.temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.coordinator.data.climate.target_temperature
|
||||
return self.coordinator.atag.climate.target_temperature
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., auto, manual, fireplace, extend, etc."""
|
||||
preset = self.coordinator.data.climate.preset_mode
|
||||
preset = self.coordinator.atag.climate.preset_mode
|
||||
return PRESET_INVERTED.get(preset)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
|
||||
await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
await self.coordinator.data.climate.set_hvac_mode(hvac_mode)
|
||||
await self.coordinator.atag.climate.set_hvac_mode(hvac_mode)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
await self.coordinator.data.climate.set_preset_mode(PRESET_MAP[preset_mode])
|
||||
await self.coordinator.atag.climate.set_preset_mode(PRESET_MAP[preset_mode])
|
||||
self.async_write_ha_state()
|
||||
|
||||
41
homeassistant/components/atag/coordinator.py
Normal file
41
homeassistant/components/atag/coordinator.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""The ATAG Integration."""
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyatag import AtagException, AtagOne
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AtagConfigEntry = ConfigEntry[AtagDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AtagDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Atag data update coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize Atag coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Atag",
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
|
||||
self.atag = AtagOne(
|
||||
session=async_get_clientsession(hass), **entry.data, device=entry.unique_id
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data via library."""
|
||||
async with timeout(20):
|
||||
try:
|
||||
await self.atag.update()
|
||||
except AtagException as err:
|
||||
raise UpdateFailed(err) from err
|
||||
@@ -1,36 +1,30 @@
|
||||
"""The ATAG Integration."""
|
||||
|
||||
from pyatag import AtagOne
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import DOMAIN
|
||||
from .coordinator import AtagDataUpdateCoordinator
|
||||
|
||||
|
||||
class AtagEntity(CoordinatorEntity[DataUpdateCoordinator[AtagOne]]):
|
||||
class AtagEntity(CoordinatorEntity[AtagDataUpdateCoordinator]):
|
||||
"""Defines a base Atag entity."""
|
||||
|
||||
def __init__(
|
||||
self, coordinator: DataUpdateCoordinator[AtagOne], atag_id: str
|
||||
) -> None:
|
||||
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None:
|
||||
"""Initialize the Atag entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._id = atag_id
|
||||
self._attr_name = DOMAIN.title()
|
||||
self._attr_unique_id = f"{coordinator.data.id}-{atag_id}"
|
||||
self._attr_unique_id = f"{coordinator.atag.id}-{atag_id}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return info for device registry."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.coordinator.data.id)},
|
||||
identifiers={(DOMAIN, self.coordinator.atag.id)},
|
||||
manufacturer="Atag",
|
||||
model="Atag One",
|
||||
name="Atag Thermostat",
|
||||
sw_version=self.coordinator.data.apiversion,
|
||||
sw_version=self.coordinator.atag.apiversion,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Initialization of ATAG One sensor platform."""
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UnitOfPressure,
|
||||
@@ -11,7 +10,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import DOMAIN
|
||||
from .coordinator import AtagConfigEntry, AtagDataUpdateCoordinator
|
||||
from .entity import AtagEntity
|
||||
|
||||
SENSORS = {
|
||||
@@ -28,43 +27,43 @@ SENSORS = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AtagConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize sensor platform from config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities([AtagSensor(coordinator, sensor) for sensor in SENSORS])
|
||||
|
||||
|
||||
class AtagSensor(AtagEntity, SensorEntity):
|
||||
"""Representation of a AtagOne Sensor."""
|
||||
|
||||
def __init__(self, coordinator, sensor):
|
||||
def __init__(self, coordinator: AtagDataUpdateCoordinator, sensor: str) -> None:
|
||||
"""Initialize Atag sensor."""
|
||||
super().__init__(coordinator, SENSORS[sensor])
|
||||
self._attr_name = sensor
|
||||
if coordinator.data.report[self._id].sensorclass in (
|
||||
if coordinator.atag.report[self._id].sensorclass in (
|
||||
SensorDeviceClass.PRESSURE,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
):
|
||||
self._attr_device_class = coordinator.data.report[self._id].sensorclass
|
||||
if coordinator.data.report[self._id].measure in (
|
||||
self._attr_device_class = coordinator.atag.report[self._id].sensorclass
|
||||
if coordinator.atag.report[self._id].measure in (
|
||||
UnitOfPressure.BAR,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
PERCENTAGE,
|
||||
UnitOfTime.HOURS,
|
||||
):
|
||||
self._attr_native_unit_of_measurement = coordinator.data.report[
|
||||
self._attr_native_unit_of_measurement = coordinator.atag.report[
|
||||
self._id
|
||||
].measure
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data.report[self._id].state
|
||||
return self.coordinator.atag.report[self._id].state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return self.coordinator.data.report[self._id].icon
|
||||
return self.coordinator.atag.report[self._id].icon
|
||||
|
||||
@@ -7,12 +7,11 @@ from homeassistant.components.water_heater import (
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import DOMAIN
|
||||
from .coordinator import AtagConfigEntry
|
||||
from .entity import AtagEntity
|
||||
|
||||
OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE]
|
||||
@@ -20,12 +19,13 @@ OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AtagConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize DHW device from config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities([AtagWaterHeater(coordinator, Platform.WATER_HEATER)])
|
||||
async_add_entities(
|
||||
[AtagWaterHeater(config_entry.runtime_data, Platform.WATER_HEATER)]
|
||||
)
|
||||
|
||||
|
||||
class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
@@ -37,30 +37,30 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.coordinator.data.dhw.temperature
|
||||
return self.coordinator.atag.dhw.temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
operation = self.coordinator.data.dhw.current_operation
|
||||
operation = self.coordinator.atag.dhw.current_operation
|
||||
return operation if operation in self.operation_list else STATE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
|
||||
if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the setpoint if water demand, otherwise return base temp (comfort level)."""
|
||||
return self.coordinator.data.dhw.target_temperature
|
||||
return self.coordinator.atag.dhw.target_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature."""
|
||||
return self.coordinator.data.dhw.max_temp
|
||||
return self.coordinator.atag.dhw.max_temp
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature."""
|
||||
return self.coordinator.data.dhw.min_temp
|
||||
return self.coordinator.atag.dhw.min_temp
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"hostname": "connect",
|
||||
"macaddress": "2C9FFB*"
|
||||
},
|
||||
{
|
||||
"hostname": "connect",
|
||||
"macaddress": "789C85*"
|
||||
},
|
||||
{
|
||||
"hostname": "august*",
|
||||
"macaddress": "E076D0*"
|
||||
|
||||
@@ -10,21 +10,15 @@
|
||||
# and add the following to the end of script/bootstrap:
|
||||
# sudo chmod 777 /dev/ttyUSB0
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AuroraAbbDataUpdateCoordinator
|
||||
from .coordinator import AuroraAbbConfigEntry, AuroraAbbDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AuroraAbbConfigEntry) -> bool:
|
||||
"""Set up Aurora ABB PowerOne from a config entry."""
|
||||
|
||||
comport = entry.data[CONF_PORT]
|
||||
@@ -32,19 +26,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = AuroraAbbDataUpdateCoordinator(hass, comport, address)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AuroraAbbConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
# It should not be necessary to close the serial port because we close
|
||||
# it after every use in sensor.py, i.e. no need to do entry["client"].close()
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -6,6 +6,7 @@ from time import sleep
|
||||
from aurorapy.client import AuroraError, AuroraSerialClient, AuroraTimeoutError
|
||||
from serial import SerialException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -14,6 +15,9 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type AuroraAbbConfigEntry = ConfigEntry[AuroraAbbDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]):
|
||||
"""Class to manage fetching AuroraAbbPowerone data."""
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_SERIAL_NUMBER,
|
||||
EntityCategory,
|
||||
@@ -31,7 +30,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AuroraAbbDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_DEVICE_NAME,
|
||||
ATTR_FIRMWARE,
|
||||
@@ -40,6 +38,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
from .coordinator import AuroraAbbConfigEntry, AuroraAbbDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ALARM_STATES = list(AuroraMapping.ALARM_STATES.values())
|
||||
@@ -130,12 +129,12 @@ SENSOR_TYPES = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AuroraAbbConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up aurora_abb_powerone sensor based on a config entry."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
data = config_entry.data
|
||||
|
||||
entities = [AuroraSensor(coordinator, data, sens) for sens in SENSOR_TYPES]
|
||||
|
||||
@@ -2,28 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aussiebb.asyncio import AussieBB
|
||||
from aussiebb.const import FETCH_TYPES
|
||||
from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType
|
||||
from aussiebb.exceptions import AuthenticationException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_ID
|
||||
from .coordinator import (
|
||||
AussieBroadbandConfigEntry,
|
||||
AussieBroadbandDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AussieBroadbandConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Aussie Broadband from a config entry."""
|
||||
# Login to the Aussie Broadband API and retrieve the current service list
|
||||
client = AussieBB(
|
||||
@@ -43,41 +42,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except ClientError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
|
||||
# Create an appropriate refresh function
|
||||
def update_data_factory(service_id):
|
||||
async def async_update_data():
|
||||
try:
|
||||
return await client.get_usage(service_id)
|
||||
except UnrecognisedServiceType as err:
|
||||
raise UpdateFailed(f"Service {service_id} was unrecognised") from err
|
||||
|
||||
return async_update_data
|
||||
|
||||
# Initiate a Data Update Coordinator for each service
|
||||
for service in services:
|
||||
service["coordinator"] = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=service["service_id"],
|
||||
update_interval=timedelta(minutes=DEFAULT_UPDATE_INTERVAL),
|
||||
update_method=update_data_factory(service[SERVICE_ID]),
|
||||
service["coordinator"] = AussieBroadbandDataUpdateCoordinator(
|
||||
hass, client, service["service_id"]
|
||||
)
|
||||
await service["coordinator"].async_config_entry_first_refresh()
|
||||
|
||||
# Setup the integration
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"client": client,
|
||||
"services": services,
|
||||
}
|
||||
entry.runtime_data = services
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AussieBroadbandConfigEntry
|
||||
) -> bool:
|
||||
"""Unload the config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Constants for the Aussie Broadband integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DEFAULT_UPDATE_INTERVAL = 30
|
||||
DOMAIN = "aussie_broadband"
|
||||
SERVICE_ID = "service_id"
|
||||
SERVICE_ID: Final = "service_id"
|
||||
CONF_SERVICES = "services"
|
||||
|
||||
53
homeassistant/components/aussie_broadband/coordinator.py
Normal file
53
homeassistant/components/aussie_broadband/coordinator.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Coordinator for the Aussie Broadband integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from aussiebb.asyncio import AussieBB
|
||||
from aussiebb.exceptions import UnrecognisedServiceType
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_UPDATE_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AussieBroadbandServiceData(TypedDict, total=False):
|
||||
"""Aussie Broadband service information, extended with the coordinator."""
|
||||
|
||||
coordinator: AussieBroadbandDataUpdateCoordinator
|
||||
description: str
|
||||
name: str
|
||||
service_id: str
|
||||
type: str
|
||||
|
||||
|
||||
type AussieBroadbandConfigEntry = ConfigEntry[list[AussieBroadbandServiceData]]
|
||||
|
||||
|
||||
class AussieBroadbandDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Aussie Broadand data update coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: AussieBB, service_id: str) -> None:
|
||||
"""Initialize Atag coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Aussie Broadband {service_id}",
|
||||
update_interval=timedelta(minutes=DEFAULT_UPDATE_INTERVAL),
|
||||
)
|
||||
self._client = client
|
||||
self._service_id = service_id
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
return await self._client.get_usage(self._service_id)
|
||||
except UnrecognisedServiceType as err:
|
||||
raise UpdateFailed(f"Service {self._service_id} was unrecognised") from err
|
||||
@@ -5,16 +5,15 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AussieBroadbandConfigEntry
|
||||
|
||||
TO_REDACT = ["address", "ipAddresses", "description", "discounts", "coordinator"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AussieBroadbandConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
@@ -23,6 +22,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"service": async_redact_data(service, TO_REDACT),
|
||||
"usage": async_redact_data(service["coordinator"].data, ["historical"]),
|
||||
}
|
||||
for service in hass.data[DOMAIN][config_entry.entry_id]["services"]
|
||||
for service in config_entry.runtime_data
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any, cast
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfInformation, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -22,6 +21,11 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, SERVICE_ID
|
||||
from .coordinator import (
|
||||
AussieBroadbandConfigEntry,
|
||||
AussieBroadbandDataUpdateCoordinator,
|
||||
AussieBroadbandServiceData,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -117,28 +121,34 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AussieBroadbandConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aussie Broadband sensor platform from a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
AussieBroadandSensorEntity(service, description)
|
||||
for service in hass.data[DOMAIN][entry.entry_id]["services"]
|
||||
for service in entry.runtime_data
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.key in service["coordinator"].data
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity):
|
||||
class AussieBroadandSensorEntity(
|
||||
CoordinatorEntity[AussieBroadbandDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Base class for Aussie Broadband metric sensors."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: SensorValueEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, service: dict[str, Any], description: SensorValueEntityDescription
|
||||
self,
|
||||
service: AussieBroadbandServiceData,
|
||||
description: SensorValueEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(service["coordinator"])
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
AwairCloudDataUpdateCoordinator,
|
||||
AwairConfigEntry,
|
||||
AwairDataUpdateCoordinator,
|
||||
AwairLocalDataUpdateCoordinator,
|
||||
)
|
||||
@@ -17,7 +16,9 @@ from .coordinator import (
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AwairConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Awair integration from a config entry."""
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
@@ -33,28 +34,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
config_entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: AwairConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
coordinator: AwairLocalDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if entry.title != coordinator.title:
|
||||
if entry.title != entry.runtime_data.title:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: AwairConfigEntry
|
||||
) -> bool:
|
||||
"""Unload Awair configuration."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
@@ -26,6 +26,8 @@ from .const import (
|
||||
UPDATE_INTERVAL_LOCAL,
|
||||
)
|
||||
|
||||
type AwairConfigEntry = ConfigEntry[AwairDataUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AwairResult:
|
||||
|
||||
@@ -46,7 +46,7 @@ from .const import (
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import AwairDataUpdateCoordinator, AwairResult
|
||||
from .coordinator import AwairConfigEntry, AwairDataUpdateCoordinator
|
||||
|
||||
DUST_ALIASES = [API_PM25, API_PM10]
|
||||
|
||||
@@ -132,15 +132,14 @@ SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AwairConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Awair sensor entity based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
entities = []
|
||||
|
||||
data: list[AwairResult] = coordinator.data.values()
|
||||
for result in data:
|
||||
for result in coordinator.data.values():
|
||||
if result.air_data:
|
||||
entities.append(AwairSensor(result.device, coordinator, SENSOR_TYPE_SCORE))
|
||||
device_sensors = result.air_data.sensors.keys()
|
||||
|
||||
@@ -16,19 +16,18 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.core import Event, HomeAssistant, State
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA, EntityFilter
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.json import ExtendedJSONEncoder
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .client import AzureDataExplorerClient
|
||||
from .const import (
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_FILTER,
|
||||
CONF_SEND_INTERVAL,
|
||||
DATA_FILTER,
|
||||
DATA_HUB,
|
||||
DEFAULT_MAX_DELAY,
|
||||
DOMAIN,
|
||||
FILTER_STATES,
|
||||
@@ -46,6 +45,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
DATA_COMPONENT: HassKey[EntityFilter] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
# fixtures for both init and config flow tests
|
||||
@@ -63,10 +63,10 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
Adds an empty filter to hass data.
|
||||
Tries to get a filter from yaml, if present set to hass data.
|
||||
"""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
if DOMAIN in yaml_config:
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
hass.data[DATA_COMPONENT] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
else:
|
||||
hass.data[DATA_COMPONENT] = FILTER_SCHEMA({})
|
||||
|
||||
return True
|
||||
|
||||
@@ -83,15 +83,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except KustoAuthenticationError:
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][DATA_HUB] = adx
|
||||
entry.async_on_unload(adx.async_stop)
|
||||
await adx.async_start()
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
adx = hass.data[DOMAIN].pop(DATA_HUB)
|
||||
await adx.async_stop()
|
||||
return True
|
||||
|
||||
|
||||
@@ -107,7 +105,7 @@ class AzureDataExplorer:
|
||||
|
||||
self.hass = hass
|
||||
self._entry = entry
|
||||
self._entities_filter = hass.data[DOMAIN][DATA_FILTER]
|
||||
self._entities_filter = hass.data[DATA_COMPONENT]
|
||||
|
||||
self._client = AzureDataExplorerClient(entry.data)
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@ CONF_APP_REG_SECRET = "client_secret"
|
||||
CONF_AUTHORITY_ID = "authority_id"
|
||||
CONF_SEND_INTERVAL = "send_interval"
|
||||
CONF_MAX_DELAY = "max_delay"
|
||||
CONF_FILTER = DATA_FILTER = "filter"
|
||||
CONF_FILTER = "filter"
|
||||
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
|
||||
DATA_HUB = "hub"
|
||||
STEP_USER = "user"
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from azure.eventhub import EventData, EventDataBatch
|
||||
@@ -19,11 +20,12 @@ from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.core import Event, HomeAssistant, State
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA, EntityFilter
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .client import AzureEventHubClient
|
||||
from .const import (
|
||||
@@ -35,13 +37,13 @@ from .const import (
|
||||
CONF_FILTER,
|
||||
CONF_MAX_DELAY,
|
||||
CONF_SEND_INTERVAL,
|
||||
DATA_FILTER,
|
||||
DATA_HUB,
|
||||
DEFAULT_MAX_DELAY,
|
||||
DOMAIN,
|
||||
FILTER_STATES,
|
||||
)
|
||||
|
||||
type AzureEventHubConfigEntry = ConfigEntry[AzureEventHub]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -61,6 +63,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
DATA_COMPONENT: HassKey[EntityFilter] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
@@ -71,10 +74,10 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
If config is empty after getting the filter, return, otherwise emit
|
||||
deprecated warning and pass the rest to the config flow.
|
||||
"""
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
if DOMAIN not in yaml_config:
|
||||
hass.data[DATA_COMPONENT] = FILTER_SCHEMA({})
|
||||
return True
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
hass.data[DATA_COMPONENT] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
|
||||
if not yaml_config[DOMAIN]:
|
||||
return True
|
||||
@@ -92,33 +95,37 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AzureEventHubConfigEntry
|
||||
) -> bool:
|
||||
"""Do the setup based on the config entry and the filter from yaml."""
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
hub = AzureEventHub(
|
||||
hass,
|
||||
entry,
|
||||
hass.data[DOMAIN][DATA_FILTER],
|
||||
hass.data[DATA_COMPONENT],
|
||||
)
|
||||
try:
|
||||
await hub.async_test_connection()
|
||||
except EventHubError as err:
|
||||
raise ConfigEntryNotReady("Could not connect to Azure Event Hub") from err
|
||||
hass.data[DOMAIN][DATA_HUB] = hub
|
||||
entry.runtime_data = hub
|
||||
entry.async_on_unload(hub.async_stop)
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_listener))
|
||||
await hub.async_start()
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_update_listener(
|
||||
hass: HomeAssistant, entry: AzureEventHubConfigEntry
|
||||
) -> None:
|
||||
"""Update listener for options."""
|
||||
hass.data[DOMAIN][DATA_HUB].update_options(entry.options)
|
||||
entry.runtime_data.update_options(entry.options)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AzureEventHubConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hub = hass.data[DOMAIN].pop(DATA_HUB)
|
||||
await hub.async_stop()
|
||||
return True
|
||||
|
||||
|
||||
@@ -129,7 +136,7 @@ class AzureEventHub:
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entities_filter: vol.Schema,
|
||||
entities_filter: EntityFilter,
|
||||
) -> None:
|
||||
"""Initialize the listener."""
|
||||
self.hass = hass
|
||||
@@ -172,7 +179,7 @@ class AzureEventHub:
|
||||
await self.async_send(None)
|
||||
await self._queue.join()
|
||||
|
||||
def update_options(self, new_options: dict[str, Any]) -> None:
|
||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> None:
|
||||
"""Update options."""
|
||||
self._send_interval = new_options[CONF_SEND_INTERVAL]
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key"
|
||||
CONF_EVENT_HUB_CON_STRING = "event_hub_connection_string"
|
||||
CONF_SEND_INTERVAL = "send_interval"
|
||||
CONF_MAX_DELAY = "max_delay"
|
||||
CONF_FILTER = DATA_FILTER = "filter"
|
||||
DATA_HUB = "hub"
|
||||
CONF_FILTER = "filter"
|
||||
|
||||
STEP_USER = "user"
|
||||
STEP_SAS = "sas"
|
||||
|
||||
@@ -13,4 +13,5 @@ EXCLUDE_FROM_BACKUP = [
|
||||
"*.log",
|
||||
"backups/*.tar",
|
||||
"OZW_Log.txt",
|
||||
"tts/*",
|
||||
]
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"pt-br": "Portugal",
|
||||
"ru-ru": "Russia",
|
||||
"sv-se": "Sweden",
|
||||
"tr-tr": "Turkey"
|
||||
"tr-tr": "Türkiye"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,46 +965,18 @@ async def async_service_temperature_set(
|
||||
ATTR_TEMPERATURE in service_call.data
|
||||
and not entity.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
):
|
||||
# Warning implemented in 2024.10 and will be changed to raising
|
||||
# a ServiceValidationError in 2025.4
|
||||
report_issue = async_suggest_report_issue(
|
||||
entity.hass,
|
||||
integration_domain=entity.platform.platform_name,
|
||||
module=type(entity).__module__,
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s set_temperature action was used with temperature but the entity does not "
|
||||
"implement the ClimateEntityFeature.TARGET_TEMPERATURE feature. "
|
||||
"This will stop working in 2025.4 and raise an error instead. "
|
||||
"Please %s"
|
||||
),
|
||||
entity.platform.platform_name,
|
||||
entity.__class__.__name__,
|
||||
report_issue,
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_target_temperature_entity_feature",
|
||||
)
|
||||
if (
|
||||
ATTR_TARGET_TEMP_LOW in service_call.data
|
||||
and not entity.supported_features
|
||||
& ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
):
|
||||
# Warning implemented in 2024.10 and will be changed to raising
|
||||
# a ServiceValidationError in 2025.4
|
||||
report_issue = async_suggest_report_issue(
|
||||
entity.hass,
|
||||
integration_domain=entity.platform.platform_name,
|
||||
module=type(entity).__module__,
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s set_temperature action was used with target_temp_low but the entity does not "
|
||||
"implement the ClimateEntityFeature.TARGET_TEMPERATURE_RANGE feature. "
|
||||
"This will stop working in 2025.4 and raise an error instead. "
|
||||
"Please %s"
|
||||
),
|
||||
entity.platform.platform_name,
|
||||
entity.__class__.__name__,
|
||||
report_issue,
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_target_temperature_range_entity_feature",
|
||||
)
|
||||
|
||||
hass = entity.hass
|
||||
|
||||
@@ -275,6 +275,12 @@
|
||||
},
|
||||
"humidity_out_of_range": {
|
||||
"message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
|
||||
},
|
||||
"missing_target_temperature_entity_feature": {
|
||||
"message": "Set temperature action was used with the target temperature parameter but the entity does not support it."
|
||||
},
|
||||
"missing_target_temperature_range_entity_feature": {
|
||||
"message": "Set temperature action was used with the target temperature low/high parameter but the entity does not support it."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ from typing import Any
|
||||
import uuid
|
||||
|
||||
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
|
||||
from homeassistant.components.automation.config import (
|
||||
PLATFORM_SCHEMA,
|
||||
async_validate_config_item,
|
||||
)
|
||||
from homeassistant.components.automation.config import async_validate_config_item
|
||||
from homeassistant.config import AUTOMATION_CONFIG_PATH
|
||||
from homeassistant.const import CONF_ID, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -48,7 +45,6 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
"config",
|
||||
AUTOMATION_CONFIG_PATH,
|
||||
cv.string,
|
||||
PLATFORM_SCHEMA,
|
||||
post_write_hook=hook,
|
||||
data_validator=async_validate_config_item,
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
"config",
|
||||
SCENE_CONFIG_PATH,
|
||||
cv.string,
|
||||
PLATFORM_SCHEMA,
|
||||
data_schema=PLATFORM_SCHEMA,
|
||||
post_write_hook=hook,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,10 +5,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
|
||||
from homeassistant.components.script.config import (
|
||||
SCRIPT_ENTITY_SCHEMA,
|
||||
async_validate_config_item,
|
||||
)
|
||||
from homeassistant.components.script.config import async_validate_config_item
|
||||
from homeassistant.config import SCRIPT_CONFIG_PATH
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -45,7 +42,6 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
"config",
|
||||
SCRIPT_CONFIG_PATH,
|
||||
cv.slug,
|
||||
SCRIPT_ENTITY_SCHEMA,
|
||||
post_write_hook=hook,
|
||||
data_validator=async_validate_config_item,
|
||||
)
|
||||
|
||||
@@ -33,9 +33,9 @@ class BaseEditConfigView[_DataT: (dict[str, dict[str, Any]], list[dict[str, Any]
|
||||
config_type: str,
|
||||
path: str,
|
||||
key_schema: Callable[[Any], str],
|
||||
data_schema: Callable[[dict[str, Any]], Any],
|
||||
*,
|
||||
post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None,
|
||||
data_schema: Callable[[dict[str, Any]], Any] | None = None,
|
||||
data_validator: Callable[
|
||||
[HomeAssistant, str, dict[str, Any]],
|
||||
Coroutine[Any, Any, dict[str, Any] | None],
|
||||
@@ -51,6 +51,12 @@ class BaseEditConfigView[_DataT: (dict[str, dict[str, Any]], list[dict[str, Any]
|
||||
self.post_write_hook = post_write_hook
|
||||
self.data_validator = data_validator
|
||||
self.mutation_lock = asyncio.Lock()
|
||||
if (self.data_schema is None and self.data_validator is None) or (
|
||||
self.data_schema is not None and self.data_validator is not None
|
||||
):
|
||||
raise ValueError(
|
||||
"Must specify exactly one of data_schema or data_validator"
|
||||
)
|
||||
|
||||
def _empty_config(self) -> _DataT:
|
||||
"""Empty config if file not found."""
|
||||
@@ -112,7 +118,8 @@ class BaseEditConfigView[_DataT: (dict[str, dict[str, Any]], list[dict[str, Any]
|
||||
if self.data_validator:
|
||||
await self.data_validator(hass, config_key, data)
|
||||
else:
|
||||
self.data_schema(data)
|
||||
# We either have a data_schema or a data_validator, ignore mypy
|
||||
self.data_schema(data) # type: ignore[misc]
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
return self.json_message(
|
||||
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["debugpy==1.8.1"]
|
||||
"requirements": ["debugpy==1.8.6"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==3.0.2"],
|
||||
"requirements": ["DoorBirdPy==3.0.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_axis-video._tcp.local.",
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"dependencies": ["mqtt"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dsmr_reader",
|
||||
"iot_class": "local_push",
|
||||
"mqtt": ["dsmr/#"]
|
||||
"mqtt": ["dsmr/#"],
|
||||
"quality_scale": "gold"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_MODEL
|
||||
|
||||
@@ -41,7 +42,10 @@ type EleventLabsConfigEntry = ConfigEntry[ElevenLabsData]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: EleventLabsConfigEntry) -> bool:
|
||||
"""Set up ElevenLabs text-to-speech from a config entry."""
|
||||
entry.add_update_listener(update_listener)
|
||||
client = AsyncElevenLabs(api_key=entry.data[CONF_API_KEY])
|
||||
httpx_client = get_async_client(hass)
|
||||
client = AsyncElevenLabs(
|
||||
api_key=entry.data[CONF_API_KEY], httpx_client=httpx_client
|
||||
)
|
||||
model_id = entry.options[CONF_MODEL]
|
||||
try:
|
||||
model = await get_model_by_id(client, model_id)
|
||||
|
||||
@@ -17,6 +17,8 @@ from homeassistant.config_entries import (
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -47,9 +49,12 @@ USER_STEP_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_voices_models(api_key: str) -> tuple[dict[str, str], dict[str, str]]:
|
||||
async def get_voices_models(
|
||||
hass: HomeAssistant, api_key: str
|
||||
) -> tuple[dict[str, str], dict[str, str]]:
|
||||
"""Get available voices and models as dicts."""
|
||||
client = AsyncElevenLabs(api_key=api_key)
|
||||
httpx_client = get_async_client(hass)
|
||||
client = AsyncElevenLabs(api_key=api_key, httpx_client=httpx_client)
|
||||
voices = (await client.voices.get_all()).voices
|
||||
models = await client.models.get_all()
|
||||
voices_dict = {
|
||||
@@ -77,7 +82,7 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
voices, _ = await get_voices_models(user_input[CONF_API_KEY])
|
||||
voices, _ = await get_voices_models(self.hass, user_input[CONF_API_KEY])
|
||||
except ApiError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
else:
|
||||
@@ -116,7 +121,7 @@ class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if not self.voices or not self.models:
|
||||
self.voices, self.models = await get_voices_models(self.api_key)
|
||||
self.voices, self.models = await get_voices_models(self.hass, self.api_key)
|
||||
|
||||
assert self.models and self.voices
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
|
||||
from elkm1_lib.discovery import ElkSystem
|
||||
from elkm1_lib.elk import Elk
|
||||
@@ -132,6 +132,8 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
host: str | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the elkm1 config flow."""
|
||||
self._discovered_device: ElkSystem | None = None
|
||||
@@ -176,10 +178,9 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if async_update_entry_from_discovery(self.hass, entry, device):
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
return self.async_abort(reason="already_configured")
|
||||
self.context[CONF_HOST] = host
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_HOST) == host:
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
self.host = host
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
# Handled ignored case since _async_current_entries
|
||||
# is called with include_ignore=False
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -190,6 +191,10 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return other_flow.host == self.host
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError
|
||||
@@ -311,6 +311,9 @@ class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.config_entry.unique_id is not None
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
@@ -326,6 +329,6 @@ class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
),
|
||||
description_placeholders={
|
||||
CONF_SERIAL: self.config_entry.unique_id,
|
||||
CONF_HOST: self.config_entry.data.get("host"),
|
||||
CONF_HOST: self.config_entry.data[CONF_HOST],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# Empty config. Updated when added to HA.
|
||||
self._satellite_config = assist_satellite.AssistSatelliteConfiguration(
|
||||
available_wake_words=[], active_wake_words=[], max_active_wake_words=0
|
||||
available_wake_words=[], active_wake_words=[], max_active_wake_words=1
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -179,7 +179,13 @@ class EsphomeAssistSatellite(
|
||||
|
||||
async def _update_satellite_config(self) -> None:
|
||||
"""Get the latest satellite configuration from the device."""
|
||||
config = await self.cli.get_voice_assistant_configuration(_CONFIG_TIMEOUT_SEC)
|
||||
try:
|
||||
config = await self.cli.get_voice_assistant_configuration(
|
||||
_CONFIG_TIMEOUT_SEC
|
||||
)
|
||||
except TimeoutError:
|
||||
# Placeholder config will be used
|
||||
return
|
||||
|
||||
# Update available/active wake words
|
||||
self._satellite_config.available_wake_words = [
|
||||
@@ -315,6 +321,10 @@ class EsphomeAssistSatellite(
|
||||
"code": event.data["code"],
|
||||
"message": event.data["message"],
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
||||
if self._tts_streaming_task is None:
|
||||
# No TTS
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
self.cli.send_voice_assistant_event(event_type, data_to_send)
|
||||
|
||||
@@ -413,7 +423,6 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# Run the pipeline
|
||||
_LOGGER.debug("Running pipeline from %s to %s", start_stage, end_stage)
|
||||
self.entry_data.async_set_assist_pipeline_state(True)
|
||||
self._pipeline_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self.async_accept_pipeline_from_satellite(
|
||||
@@ -443,7 +452,6 @@ class EsphomeAssistSatellite(
|
||||
|
||||
def handle_pipeline_finished(self) -> None:
|
||||
"""Handle when pipeline has finished running."""
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
self._stop_udp_server()
|
||||
_LOGGER.debug("Pipeline finished")
|
||||
|
||||
@@ -561,6 +569,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# State change
|
||||
self.tts_response_finished()
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
async def _wrap_audio_stream(self) -> AsyncIterable[bytes]:
|
||||
"""Yield audio chunks from the queue until None."""
|
||||
|
||||
@@ -68,7 +68,7 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity):
|
||||
@convert_api_error_ha_error
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock."""
|
||||
code = kwargs.get(ATTR_CODE, None)
|
||||
code = kwargs.get(ATTR_CODE)
|
||||
self._client.lock_command(self._key, LockCommand.UNLOCK, code)
|
||||
|
||||
@convert_api_error_ha_error
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"assist_satellite": {
|
||||
"assist_satellite": {
|
||||
"name": "[%key:component::assist_satellite::entity_component::_::name%]"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"assist_in_progress": {
|
||||
"name": "[%key:component::assist_pipeline::entity::binary_sensor::assist_in_progress::name%]"
|
||||
|
||||
@@ -19,6 +19,7 @@ from .coordinator import FeedReaderCoordinator
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CONTENT = "content"
|
||||
ATTR_DESCRIPTION = "description"
|
||||
ATTR_LINK = "link"
|
||||
ATTR_TITLE = "title"
|
||||
|
||||
@@ -40,7 +41,9 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity):
|
||||
_attr_event_types = [EVENT_FEEDREADER]
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
_unrecorded_attributes = frozenset({ATTR_CONTENT, ATTR_TITLE, ATTR_LINK})
|
||||
_unrecorded_attributes = frozenset(
|
||||
{ATTR_CONTENT, ATTR_DESCRIPTION, ATTR_TITLE, ATTR_LINK}
|
||||
)
|
||||
coordinator: FeedReaderCoordinator
|
||||
|
||||
def __init__(self, coordinator: FeedReaderCoordinator) -> None:
|
||||
@@ -80,6 +83,7 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity):
|
||||
self._trigger_event(
|
||||
EVENT_FEEDREADER,
|
||||
{
|
||||
ATTR_DESCRIPTION: feed_data.get("description"),
|
||||
ATTR_TITLE: feed_data.get("title"),
|
||||
ATTR_LINK: feed_data.get("link"),
|
||||
ATTR_CONTENT: content,
|
||||
|
||||
@@ -241,11 +241,14 @@ class FibaroController:
|
||||
platform = Platform.LOCK
|
||||
elif device.has_central_scene_event:
|
||||
platform = Platform.EVENT
|
||||
elif device.value.has_value:
|
||||
if device.value.is_bool_value:
|
||||
platform = Platform.BINARY_SENSOR
|
||||
else:
|
||||
platform = Platform.SENSOR
|
||||
elif device.value.has_value and device.value.is_bool_value:
|
||||
platform = Platform.BINARY_SENSOR
|
||||
elif (
|
||||
device.value.has_value
|
||||
or "power" in device.properties
|
||||
or "energy" in device.properties
|
||||
):
|
||||
platform = Platform.SENSOR
|
||||
|
||||
# Switches that control lights should show up as lights
|
||||
if platform == Platform.SWITCH and device.properties.get("isLight", False):
|
||||
|
||||
@@ -112,6 +112,11 @@ async def async_setup_entry(
|
||||
entities: list[SensorEntity] = [
|
||||
FibaroSensor(device, MAIN_SENSOR_TYPES.get(device.type))
|
||||
for device in controller.fibaro_devices[Platform.SENSOR]
|
||||
# Some sensor devices do not have a value but report power or energy.
|
||||
# These sensors are added to the sensor list but need to be excluded
|
||||
# here as the FibaroSensor expects a value. One example is the
|
||||
# Qubino 3 phase power meter.
|
||||
if device.value.has_value
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from fjaraskupan import Device, State
|
||||
from fjaraskupan import (
|
||||
Device,
|
||||
FjaraskupanConnectionError,
|
||||
FjaraskupanError,
|
||||
FjaraskupanReadError,
|
||||
FjaraskupanWriteError,
|
||||
State,
|
||||
)
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
@@ -19,9 +26,37 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def exception_converter():
|
||||
"""Convert exception so home assistant translated ones."""
|
||||
|
||||
try:
|
||||
yield
|
||||
except FjaraskupanWriteError as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="write_error"
|
||||
) from exception
|
||||
except FjaraskupanReadError as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="read_error"
|
||||
) from exception
|
||||
except FjaraskupanConnectionError as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="connection_error"
|
||||
) from exception
|
||||
except FjaraskupanError as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unexpected_error",
|
||||
translation_placeholders={"msg": str(exception)},
|
||||
) from exception
|
||||
|
||||
|
||||
class UnableToConnect(HomeAssistantError):
|
||||
"""Exception to indicate that we cannot connect to device."""
|
||||
|
||||
@@ -71,8 +106,11 @@ class FjaraskupanCoordinator(DataUpdateCoordinator[State]):
|
||||
)
|
||||
) is None:
|
||||
raise UpdateFailed("No connectable path to device")
|
||||
async with self.device.connect(ble_device) as device:
|
||||
await device.update()
|
||||
|
||||
with exception_converter():
|
||||
async with self.device.connect(ble_device) as device:
|
||||
await device.update()
|
||||
|
||||
return self.device.state
|
||||
|
||||
def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||
@@ -90,7 +128,8 @@ class FjaraskupanCoordinator(DataUpdateCoordinator[State]):
|
||||
) is None:
|
||||
raise UnableToConnect("No connectable path to device")
|
||||
|
||||
async with self.device.connect(ble_device) as device:
|
||||
yield device
|
||||
with exception_converter():
|
||||
async with self.device.connect(ble_device) as device:
|
||||
yield device
|
||||
|
||||
self.async_set_updated_data(self.device.state)
|
||||
|
||||
@@ -24,5 +24,19 @@
|
||||
"name": "Periodic venting"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"write_error": {
|
||||
"message": "Failed to write data to device"
|
||||
},
|
||||
"read_error": {
|
||||
"message": "Failed to read data from device"
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Failed to connect to device"
|
||||
},
|
||||
"unexpected_error": {
|
||||
"message": "Unexpected error occurred: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import Any, cast
|
||||
from typing import Any, Self, cast
|
||||
|
||||
from flux_led.const import (
|
||||
ATTR_ID,
|
||||
@@ -61,6 +61,8 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
host: str | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_devices: dict[str, FluxLEDDiscovery] = {}
|
||||
@@ -149,10 +151,9 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert device is not None
|
||||
await self._async_set_discovered_mac(device, self._allow_update_mac)
|
||||
host = device[ATTR_IPADDR]
|
||||
self.context[CONF_HOST] = host
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_HOST) == host:
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
self.host = host
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
if not device[ATTR_MODEL_DESCRIPTION]:
|
||||
mac_address = device[ATTR_ID]
|
||||
assert mac_address is not None
|
||||
@@ -173,6 +174,10 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self._async_set_discovered_mac(device, True)
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return other_flow.host == self.host
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
from urllib.parse import ParseResult, urlparse
|
||||
|
||||
from fritzconnection import FritzConnection
|
||||
@@ -155,7 +155,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
|
||||
or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
|
||||
)
|
||||
self.context[CONF_HOST] = self._host
|
||||
|
||||
if not self._host or ipaddress.ip_address(self._host).is_link_local:
|
||||
return self.async_abort(reason="ignore_ip6_link_local")
|
||||
@@ -166,9 +165,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self._host})
|
||||
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_HOST) == self._host:
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
if entry := await self.async_check_configured_entry():
|
||||
if uuid and not entry.unique_id:
|
||||
@@ -184,6 +182,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return other_flow._host == self._host # noqa: SLF001
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -33,6 +33,7 @@ from .const import (
|
||||
from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator
|
||||
from .entity import FritzBoxDeviceEntity
|
||||
from .model import ClimateExtraAttributes
|
||||
from .sensor import value_scheduled_preset
|
||||
|
||||
HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF]
|
||||
PRESET_HOLIDAY = "holiday"
|
||||
@@ -177,7 +178,11 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
else:
|
||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||
target_temp = self.data.eco_temperature
|
||||
else:
|
||||
target_temp = self.data.comfort_temperature
|
||||
await self.async_set_temperature(temperature=target_temp)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pyfritzhome import Fritzhome, LoginError
|
||||
@@ -122,7 +122,6 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by discovery."""
|
||||
host = urlparse(discovery_info.ssdp_location).hostname
|
||||
assert isinstance(host, str)
|
||||
self.context[CONF_HOST] = host
|
||||
|
||||
if (
|
||||
ipaddress.ip_address(host).version == 6
|
||||
@@ -136,9 +135,9 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_HOST) == host:
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
self._host = host
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
# update old and user-configured config entries
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
@@ -147,12 +146,15 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
self._host = host
|
||||
self._name = str(discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or host)
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._name}
|
||||
return await self.async_step_confirm()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return other_flow._host == self._host # noqa: SLF001
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240925.0"]
|
||||
"requirements": ["home-assistant-frontend==20240930.0"]
|
||||
}
|
||||
|
||||
@@ -155,8 +155,22 @@ type GeniusHubConfigEntry = ConfigEntry[GeniusBroker]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) -> bool:
|
||||
"""Create a Genius Hub system."""
|
||||
if CONF_TOKEN in entry.data and CONF_MAC in entry.data:
|
||||
entity_registry = er.async_get(hass)
|
||||
registry_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
for reg_entry in registry_entries:
|
||||
if reg_entry.unique_id.startswith(entry.data[CONF_MAC]):
|
||||
entity_registry.async_update_entity(
|
||||
reg_entry.entity_id,
|
||||
new_unique_id=reg_entry.unique_id.replace(
|
||||
entry.data[CONF_MAC], entry.entry_id
|
||||
),
|
||||
)
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
unique_id: str
|
||||
if CONF_HOST in entry.data:
|
||||
client = GeniusHub(
|
||||
entry.data[CONF_HOST],
|
||||
@@ -164,14 +178,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) ->
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
unique_id = entry.data[CONF_MAC]
|
||||
else:
|
||||
client = GeniusHub(entry.data[CONF_TOKEN], session=session)
|
||||
unique_id = entry.entry_id
|
||||
|
||||
unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
broker = entry.runtime_data = GeniusBroker(
|
||||
hass, client, entry.data.get(CONF_MAC, unique_id)
|
||||
)
|
||||
broker = entry.runtime_data = GeniusBroker(hass, client, unique_id)
|
||||
|
||||
try:
|
||||
await client.update()
|
||||
|
||||
@@ -57,44 +57,17 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
|
||||
def __init__(self, device, gps=None, location_name=None, attributes=None):
|
||||
"""Set up Geofency entity."""
|
||||
self._attributes = attributes or {}
|
||||
self._attr_extra_state_attributes = attributes or {}
|
||||
self._name = device
|
||||
self._location_name = location_name
|
||||
self._gps = gps
|
||||
self._attr_location_name = location_name
|
||||
if gps:
|
||||
self._attr_latitude = gps[0]
|
||||
self._attr_longitude = gps[1]
|
||||
self._unsub_dispatcher = None
|
||||
self._unique_id = device
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return device specific attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
return self._gps[0]
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
return self._gps[1]
|
||||
|
||||
@property
|
||||
def location_name(self):
|
||||
"""Return a location name for the current location of the device."""
|
||||
return self._location_name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
identifiers={(GF_DOMAIN, self._unique_id)},
|
||||
name=self._name,
|
||||
self._attr_unique_id = device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(GF_DOMAIN, device)},
|
||||
name=device,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -104,21 +77,23 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
self.hass, TRACKER_UPDATE, self._async_receive_data
|
||||
)
|
||||
|
||||
if self._attributes:
|
||||
if self._attr_extra_state_attributes:
|
||||
return
|
||||
|
||||
if (state := await self.async_get_last_state()) is None:
|
||||
self._gps = (None, None)
|
||||
self._attr_latitude = None
|
||||
self._attr_longitude = None
|
||||
return
|
||||
|
||||
attr = state.attributes
|
||||
self._gps = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
|
||||
self._attr_latitude = attr.get(ATTR_LATITUDE)
|
||||
self._attr_longitude = attr.get(ATTR_LONGITUDE)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Clean up after entity before removal."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._unsub_dispatcher()
|
||||
self.hass.data[GF_DOMAIN]["devices"].remove(self._unique_id)
|
||||
self.hass.data[GF_DOMAIN]["devices"].remove(self.unique_id)
|
||||
|
||||
@callback
|
||||
def _async_receive_data(self, device, gps, location_name, attributes):
|
||||
@@ -126,7 +101,8 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
if device != self._name:
|
||||
return
|
||||
|
||||
self._attributes.update(attributes)
|
||||
self._location_name = location_name
|
||||
self._gps = gps
|
||||
self._attr_extra_state_attributes.update(attributes)
|
||||
self._attr_location_name = location_name
|
||||
self._attr_latitude = gps[0]
|
||||
self._attr_longitude = gps[1]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Self
|
||||
|
||||
from ismartgate.common import AbstractInfoResponse, ApiError
|
||||
from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode
|
||||
@@ -57,19 +57,21 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def _async_discovery_handler(self, ip_address: str) -> ConfigFlowResult:
|
||||
"""Start the user flow from any discovery."""
|
||||
self.context[CONF_IP_ADDRESS] = ip_address
|
||||
self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address})
|
||||
|
||||
self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
|
||||
|
||||
self._ip_address = ip_address
|
||||
for progress in self._async_in_progress():
|
||||
if progress.get("context", {}).get(CONF_IP_ADDRESS) == self._ip_address:
|
||||
raise AbortFlow("already_in_progress")
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
raise AbortFlow("already_in_progress")
|
||||
|
||||
self._device_type = DEVICE_TYPE_ISMARTGATE
|
||||
return await self.async_step_user()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return other_flow._ip_address == self._ip_address # noqa: SLF001
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"]
|
||||
"requirements": ["gcal-sync==6.1.5", "oauth2client==4.1.3", "ical==8.2.0"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Media source for Google Photos."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
@@ -46,7 +48,7 @@ class PhotosIdentifierType(StrEnum):
|
||||
ALBUM = "a"
|
||||
|
||||
@classmethod
|
||||
def of(cls, name: str) -> "PhotosIdentifierType":
|
||||
def of(cls, name: str) -> PhotosIdentifierType:
|
||||
"""Parse a PhotosIdentifierType by string value."""
|
||||
for enum in PhotosIdentifierType:
|
||||
if enum.value == name:
|
||||
|
||||
@@ -71,52 +71,25 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
|
||||
def __init__(self, device, location, battery, accuracy, attributes):
|
||||
"""Set up GPSLogger entity."""
|
||||
self._accuracy = accuracy
|
||||
self._attributes = attributes
|
||||
self._attr_location_accuracy = accuracy
|
||||
self._attr_extra_state_attributes = attributes
|
||||
self._name = device
|
||||
self._battery = battery
|
||||
self._location = location
|
||||
if location:
|
||||
self._attr_latitude = location[0]
|
||||
self._attr_longitude = location[1]
|
||||
self._unsub_dispatcher = None
|
||||
self._unique_id = device
|
||||
self._attr_unique_id = device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(GPL_DOMAIN, device)},
|
||||
name=device,
|
||||
)
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return battery value of the device."""
|
||||
return self._battery
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return device specific attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
return self._location[0]
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
return self._location[1]
|
||||
|
||||
@property
|
||||
def location_accuracy(self):
|
||||
"""Return the gps accuracy of the device."""
|
||||
return self._accuracy
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return DeviceInfo(
|
||||
identifiers={(GPL_DOMAIN, self._unique_id)},
|
||||
name=self._name,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -125,13 +98,14 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
# don't restore if we got created with data
|
||||
if self._location is not None:
|
||||
if self.latitude is not None:
|
||||
return
|
||||
|
||||
if (state := await self.async_get_last_state()) is None:
|
||||
self._location = (None, None)
|
||||
self._accuracy = None
|
||||
self._attributes = {
|
||||
self._attr_latitude = None
|
||||
self._attr_longitude = None
|
||||
self._attr_location_accuracy = 0
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ALTITUDE: None,
|
||||
ATTR_ACTIVITY: None,
|
||||
ATTR_DIRECTION: None,
|
||||
@@ -142,9 +116,10 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
attr = state.attributes
|
||||
self._location = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
|
||||
self._accuracy = attr.get(ATTR_GPS_ACCURACY)
|
||||
self._attributes = {
|
||||
self._attr_latitude = attr.get(ATTR_LATITUDE)
|
||||
self._attr_longitude = attr.get(ATTR_LONGITUDE)
|
||||
self._attr_location_accuracy = attr.get(ATTR_GPS_ACCURACY, 0)
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE),
|
||||
ATTR_ACTIVITY: attr.get(ATTR_ACTIVITY),
|
||||
ATTR_DIRECTION: attr.get(ATTR_DIRECTION),
|
||||
@@ -164,8 +139,9 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity):
|
||||
if device != self._name:
|
||||
return
|
||||
|
||||
self._location = location
|
||||
self._attr_latitude = location[0]
|
||||
self._attr_longitude = location[1]
|
||||
self._battery = battery
|
||||
self._accuracy = accuracy
|
||||
self._attributes.update(attributes)
|
||||
self._attr_location_accuracy = accuracy
|
||||
self._attr_extra_state_attributes.update(attributes)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
LightState,
|
||||
filter_supported_color_modes,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -42,7 +43,6 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
@@ -207,7 +207,7 @@ class LightGroup(GroupEntity, LightEntity):
|
||||
for entity_id in self._entity_ids
|
||||
if (state := self.hass.states.get(entity_id)) is not None
|
||||
]
|
||||
on_states = [state for state in states if state.state == STATE_ON]
|
||||
on_states = [state for state in states if state.state == LightState.ON]
|
||||
|
||||
valid_state = self.mode(
|
||||
state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
|
||||
@@ -218,7 +218,9 @@ class LightGroup(GroupEntity, LightEntity):
|
||||
self._attr_is_on = None
|
||||
else:
|
||||
# Set as ON if any / all member is ON
|
||||
self._attr_is_on = self.mode(state.state == STATE_ON for state in states)
|
||||
self._attr_is_on = self.mode(
|
||||
state.state == LightState.ON for state in states
|
||||
)
|
||||
|
||||
self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
|
||||
self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.1.0b1"]
|
||||
"requirements": ["aiohasupervisor==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -127,5 +127,5 @@ class HiveSensorEntity(HiveEntity, SensorEntity):
|
||||
await self.hive.session.updateData(self.device)
|
||||
self.device = await self.hive.sensor.getSensor(self.device)
|
||||
self._attr_native_value = self.entity_description.fn(
|
||||
self.device["status"]["state"]
|
||||
self.device.get("status", {}).get("state")
|
||||
)
|
||||
|
||||
@@ -22,16 +22,12 @@ from homeassistant.components.light import (
|
||||
ATTR_WHITE,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightState,
|
||||
brightness_supported,
|
||||
color_supported,
|
||||
color_temp_supported,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.core import CALLBACK_TYPE, State, callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.color import (
|
||||
@@ -244,7 +240,7 @@ class Light(HomeAccessory):
|
||||
state = new_state.state
|
||||
attributes = new_state.attributes
|
||||
color_mode = attributes.get(ATTR_COLOR_MODE)
|
||||
self.char_on.set_value(int(state == STATE_ON))
|
||||
self.char_on.set_value(int(state == LightState.ON))
|
||||
color_mode_changed = self._previous_color_mode != color_mode
|
||||
self._previous_color_mode = color_mode
|
||||
|
||||
@@ -265,7 +261,7 @@ class Light(HomeAccessory):
|
||||
# Therefore, if the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0 and state == STATE_ON:
|
||||
if brightness == 0 and state == LightState.ON:
|
||||
brightness = 1
|
||||
self.char_brightness.set_value(brightness)
|
||||
if color_mode_changed:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any, Self, cast
|
||||
|
||||
import aiohomekit
|
||||
from aiohomekit import Controller, const as aiohomekit_const
|
||||
@@ -111,6 +111,8 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.devices: dict[str, AbstractDiscovery] = {}
|
||||
self.controller: Controller | None = None
|
||||
self.finish_pairing: FinishPairing | None = None
|
||||
self.pairing = False
|
||||
self._device_paired = False
|
||||
|
||||
async def _async_setup_controller(self) -> None:
|
||||
"""Create the controller."""
|
||||
@@ -300,18 +302,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Set unique-id and error out if it's already configured
|
||||
self._abort_if_unique_id_configured(updates=updated_ip_port)
|
||||
|
||||
for progress in self._async_in_progress(include_uninitialized=True):
|
||||
context = progress["context"]
|
||||
if context.get("unique_id") == normalized_hkid and not context.get(
|
||||
"pairing"
|
||||
):
|
||||
if paired:
|
||||
# If the device gets paired, we want to dismiss
|
||||
# an existing discovery since we can no longer
|
||||
# pair with it
|
||||
self.hass.config_entries.flow.async_abort(progress["flow_id"])
|
||||
else:
|
||||
raise AbortFlow("already_in_progress")
|
||||
self.hkid = normalized_hkid
|
||||
self._device_paired = paired
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
raise AbortFlow("already_in_progress")
|
||||
|
||||
if paired:
|
||||
# Device is paired but not to us - ignore it
|
||||
@@ -332,13 +326,24 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.name = name
|
||||
self.model = model
|
||||
self.category = Categories(int(properties.get("ci", 0)))
|
||||
self.hkid = normalized_hkid
|
||||
|
||||
# We want to show the pairing form - but don't call async_step_pair
|
||||
# directly as it has side effects (will ask the device to show a
|
||||
# pairing code)
|
||||
return self._async_step_pair_show_form()
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
if other_flow.context.get("unique_id") == self.hkid and not other_flow.pairing:
|
||||
if self._device_paired:
|
||||
# If the device gets paired, we want to dismiss
|
||||
# an existing discovery since we can no longer
|
||||
# pair with it
|
||||
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
@@ -419,7 +424,7 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
assert self.controller
|
||||
|
||||
if pair_info and self.finish_pairing:
|
||||
self.context["pairing"] = True
|
||||
self.pairing = True
|
||||
code = pair_info["pairing_code"]
|
||||
try:
|
||||
code = ensure_pin_format(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Config flow for the html5 component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -42,7 +44,7 @@ class HTML5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@callback
|
||||
def _async_create_html5_entry(
|
||||
self: "HTML5ConfigFlow", data: dict[str, str]
|
||||
self: HTML5ConfigFlow, data: dict[str, str]
|
||||
) -> tuple[dict[str, str], ConfigFlowResult | None]:
|
||||
"""Create an HTML5 entry."""
|
||||
errors = {}
|
||||
@@ -68,7 +70,7 @@ class HTML5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return errors, flow_result
|
||||
|
||||
async def async_step_user(
|
||||
self: "HTML5ConfigFlow", user_input: dict[str, Any] | None = None
|
||||
self: HTML5ConfigFlow, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
@@ -92,7 +94,7 @@ class HTML5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self: "HTML5ConfigFlow", import_config: dict
|
||||
self: HTML5ConfigFlow, import_config: dict
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle config import from yaml."""
|
||||
_, flow_result = self._async_create_html5_entry(import_config)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user