mirror of
https://github.com/home-assistant/core.git
synced 2025-09-20 10:29:26 +00:00
Compare commits
216 Commits
trigger_de
...
drop-ignor
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1bfac54e56 | ||
![]() |
0eaea13e8d | ||
![]() |
b1e4513f7d | ||
![]() |
6d7f8bb7d7 | ||
![]() |
b481aaba77 | ||
![]() |
d539f37aa4 | ||
![]() |
865b3a6646 | ||
![]() |
1c603f968f | ||
![]() |
d821d27730 | ||
![]() |
dfa060a7e1 | ||
![]() |
5262cca8e6 | ||
![]() |
2c36a74da5 | ||
![]() |
084cde6ecf | ||
![]() |
3e34aa5fb7 | ||
![]() |
268f0d9e03 | ||
![]() |
f8d3bc1b89 | ||
![]() |
fb64ff1d17 | ||
![]() |
ff72faf83a | ||
![]() |
acb58c41eb | ||
![]() |
586b197fc3 | ||
![]() |
5c1d16d582 | ||
![]() |
73be4625ae | ||
![]() |
775701133d | ||
![]() |
1af0282091 | ||
![]() |
c876bed33f | ||
![]() |
e9d39a826e | ||
![]() |
f9e1c07c04 | ||
![]() |
c0bef51563 | ||
![]() |
b41a9575af | ||
![]() |
e585b3abd1 | ||
![]() |
5d2877f454 | ||
![]() |
2d89c60ac5 | ||
![]() |
860a7b7d91 | ||
![]() |
5585376b40 | ||
![]() |
c4cb70fc06 | ||
![]() |
981ae39182 | ||
![]() |
dff4f79925 | ||
![]() |
bf64e11960 | ||
![]() |
823d20c67f | ||
![]() |
1a654cd35d | ||
![]() |
13e592edaf | ||
![]() |
94191239c6 | ||
![]() |
91a1ca09f7 | ||
![]() |
9f1fe8a067 | ||
![]() |
f2c9cdb09e | ||
![]() |
712115cdb8 | ||
![]() |
eb6ae9d2d6 | ||
![]() |
b126f3fa66 | ||
![]() |
2d720f0d32 | ||
![]() |
c0155f5e80 | ||
![]() |
23a2d69984 | ||
![]() |
a8779d5f52 | ||
![]() |
01c197e830 | ||
![]() |
ef4f476844 | ||
![]() |
8aee05b8b0 | ||
![]() |
0f3f8d5707 | ||
![]() |
2948b1c58e | ||
![]() |
4cb2af4d08 | ||
![]() |
8e12d2028d | ||
![]() |
5b046def8e | ||
![]() |
6a81bf6f5e | ||
![]() |
102d6a37c0 | ||
![]() |
fd6aba3022 | ||
![]() |
a88eadf863 | ||
![]() |
52f0d04c38 | ||
![]() |
3ab80c6ff2 | ||
![]() |
71485871c8 | ||
![]() |
ba0da4c2a3 | ||
![]() |
cbaadebac3 | ||
![]() |
fd0ae32058 | ||
![]() |
382bf78ee0 | ||
![]() |
6aa077a48d | ||
![]() |
b638fcbaad | ||
![]() |
704edac9fd | ||
![]() |
ff9e2a8f1e | ||
![]() |
d778afe61a | ||
![]() |
448084e2b5 | ||
![]() |
d99379ffdf | ||
![]() |
b835b7f266 | ||
![]() |
e96e97edca | ||
![]() |
df7c657d7e | ||
![]() |
4f5502ab47 | ||
![]() |
c30ee776e9 | ||
![]() |
efebdc0181 | ||
![]() |
da7fc88f1f | ||
![]() |
566aeb5e9a | ||
![]() |
d17f0ef55a | ||
![]() |
35025c4b59 | ||
![]() |
e5d512d5e5 | ||
![]() |
2b5028bfb7 | ||
![]() |
757fee9f73 | ||
![]() |
06130219b4 | ||
![]() |
4e2fe63182 | ||
![]() |
d0cc9990dd | ||
![]() |
76ca9ce3a4 | ||
![]() |
124e7cf4c8 | ||
![]() |
260ea9a3be | ||
![]() |
e1f6820cb6 | ||
![]() |
2215777cfb | ||
![]() |
fa3ce62ae8 | ||
![]() |
33421bddf3 | ||
![]() |
1efe2b437d | ||
![]() |
a54f0adf74 | ||
![]() |
afe574f74e | ||
![]() |
25aae8944d | ||
![]() |
f26e6ad211 | ||
![]() |
e9444a2e4d | ||
![]() |
60988534a9 | ||
![]() |
932bf81ac8 | ||
![]() |
1302b6744e | ||
![]() |
0aeff366bd | ||
![]() |
0db23b0da6 | ||
![]() |
863e2074b6 | ||
![]() |
13828f6713 | ||
![]() |
fdb38ec8ec | ||
![]() |
55abb6e594 | ||
![]() |
a83e4f5c63 | ||
![]() |
cba15ee439 | ||
![]() |
400620399a | ||
![]() |
28e19215ad | ||
![]() |
119d0a0170 | ||
![]() |
69faf38e86 | ||
![]() |
d0ef1a1a8b | ||
![]() |
8f328810bf | ||
![]() |
4f1b75e3b4 | ||
![]() |
445a7fc749 | ||
![]() |
977c0797aa | ||
![]() |
a24f027923 | ||
![]() |
7b45798e30 | ||
![]() |
2b0cda0ad1 | ||
![]() |
12dca4b1bf | ||
![]() |
8c509b11b2 | ||
![]() |
991c9008bd | ||
![]() |
fe95f6e1c5 | ||
![]() |
37510aa316 | ||
![]() |
4e40e9bf74 | ||
![]() |
70c9b1f095 | ||
![]() |
f714388130 | ||
![]() |
ffb2a693f4 | ||
![]() |
9d8e253ad3 | ||
![]() |
31631cc882 | ||
![]() |
3a64357201 | ||
![]() |
20fdec9e9c | ||
![]() |
064a63fe1f | ||
![]() |
803654223a | ||
![]() |
a6148b50cf | ||
![]() |
02a3c5be14 | ||
![]() |
08ea640629 | ||
![]() |
7dd761c9c3 | ||
![]() |
6b827dfc33 | ||
![]() |
67c19087dd | ||
![]() |
55c7c2f730 | ||
![]() |
afee936c3d | ||
![]() |
ed2ced6c36 | ||
![]() |
4c5cf028d7 | ||
![]() |
68faa897ad | ||
![]() |
53c9c42148 | ||
![]() |
d48cc03be7 | ||
![]() |
28236aa023 | ||
![]() |
bfae07135a | ||
![]() |
99d580e371 | ||
![]() |
4d53450cbf | ||
![]() |
1fbce01e26 | ||
![]() |
a9621ac811 | ||
![]() |
94f2118b19 | ||
![]() |
73ca6b4900 | ||
![]() |
31e647b5b0 | ||
![]() |
fac5b2c09c | ||
![]() |
ae48179e95 | ||
![]() |
88c9d5dbe3 | ||
![]() |
b76f47cd9f | ||
![]() |
822e1ffc8d | ||
![]() |
1632e0aef6 | ||
![]() |
e2bc73f153 | ||
![]() |
46cfdddc80 | ||
![]() |
0bdf6757c4 | ||
![]() |
312e590360 | ||
![]() |
7a6aaf667b | ||
![]() |
33eaca24d6 | ||
![]() |
3d27d501b1 | ||
![]() |
39b651e075 | ||
![]() |
a962777a2e | ||
![]() |
594ce8f266 | ||
![]() |
9f867f268c | ||
![]() |
9edd242734 | ||
![]() |
93e11aa8bc | ||
![]() |
c2b298283e | ||
![]() |
106c086e8b | ||
![]() |
cbf4130bff | ||
![]() |
afffe0b08b | ||
![]() |
c1ccfee7cc | ||
![]() |
8d8383e1c1 | ||
![]() |
f350a1a1fa | ||
![]() |
fe2bd8d09e | ||
![]() |
cf14226b02 | ||
![]() |
bd3fe1d4ad | ||
![]() |
377ca04be8 | ||
![]() |
5837f55205 | ||
![]() |
0766edb9c4 | ||
![]() |
e62e3778f3 | ||
![]() |
aa8e4c1c15 | ||
![]() |
46ed8a73fc | ||
![]() |
83f22497ae | ||
![]() |
3dda1685dc | ||
![]() |
6fa9d42401 | ||
![]() |
1a54d566f8 | ||
![]() |
1a9cae0f89 | ||
![]() |
551dcaa169 | ||
![]() |
5467db065b | ||
![]() |
6a8d752e56 | ||
![]() |
179a56628d | ||
![]() |
b3f830773a | ||
![]() |
084e06ec7d | ||
![]() |
e0190afd3c | ||
![]() |
b9e16d54c4 | ||
![]() |
627785edc1 |
14
.github/workflows/builder.yml
vendored
14
.github/workflows/builder.yml
vendored
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -330,14 +330,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -462,7 +462,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -502,7 +502,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@@ -255,7 +255,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -301,7 +301,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -341,7 +341,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -381,7 +381,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -593,7 +593,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -626,7 +626,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -683,7 +683,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -726,7 +726,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -741,14 +741,14 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y homeassistant
|
||||
pylint homeassistant
|
||||
- name: Run pylint (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
pylint-tests:
|
||||
name: Check pylint on tests
|
||||
@@ -773,7 +773,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -825,7 +825,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -833,7 +833,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -895,7 +895,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -956,7 +956,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -970,7 +970,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1089,7 +1089,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1231,7 +1231,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1336,7 +1336,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1390,7 +1390,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1486,7 +1486,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1511,7 +1511,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.29.5
|
||||
uses: github/codeql-action/init@v3.29.7
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.5
|
||||
uses: github/codeql-action/analyze@v3.29.7
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
@@ -231,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v1.2.8
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v1.2.8
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
14
.github/workflows/wheels.yml
vendored
14
.github/workflows/wheels.yml
vendored
@@ -138,17 +138,17 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -187,22 +187,22 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
@@ -310,7 +310,6 @@ homeassistant.components.letpot.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linear_garage_door.*
|
||||
homeassistant.components.linkplay.*
|
||||
homeassistant.components.litejet.*
|
||||
homeassistant.components.litterrobot.*
|
||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -862,8 +862,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
/tests/components/linear_garage_door/ @IceBotYT
|
||||
/homeassistant/components/linkplay/ @Velleman
|
||||
/tests/components/linkplay/ @Velleman
|
||||
/homeassistant/components/linux_battery/ @fabaff
|
||||
@@ -1599,6 +1597,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/todo/ @home-assistant/core
|
||||
/homeassistant/components/todoist/ @boralyl
|
||||
/tests/components/todoist/ @boralyl
|
||||
/homeassistant/components/togrill/ @elupus
|
||||
/tests/components/togrill/ @elupus
|
||||
/homeassistant/components/tolo/ @MatthiasLohr
|
||||
/tests/components/tolo/ @MatthiasLohr
|
||||
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
|
||||
@@ -1613,8 +1613,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tplink_omada/ @MarkGodwin
|
||||
/homeassistant/components/traccar/ @ludeeus
|
||||
/tests/components/traccar/ @ludeeus
|
||||
/homeassistant/components/traccar_server/ @ludeeus
|
||||
/tests/components/traccar_server/ @ludeeus
|
||||
/homeassistant/components/trace/ @home-assistant/core
|
||||
/tests/components/trace/ @home-assistant/core
|
||||
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
||||
|
@@ -120,6 +120,9 @@ class AuthStore:
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
while new_user.id in self._users:
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
self._users[new_user.id] = new_user
|
||||
|
||||
if credentials is None:
|
||||
|
@@ -10,7 +10,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
|
106
homeassistant/components/airos/binary_sensor.py
Normal file
106
homeassistant/components/airos/binary_sensor.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""AirOS Binary Sensor component for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||
from .entity import AirOSEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe an AirOS binary sensor."""
|
||||
|
||||
value_fn: Callable[[AirOSData], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="portfw",
|
||||
translation_key="port_forwarding",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.portfw,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp_client",
|
||||
translation_key="dhcp_client",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcpc,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp_server",
|
||||
translation_key="dhcp_server",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcpd,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp6_server",
|
||||
translation_key="dhcp6_server",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcp6d_stateful,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="pppoe",
|
||||
translation_key="pppoe",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.pppoe,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirOSConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the AirOS binary sensors from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
entity_description: AirOSBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirOSDataUpdateCoordinator,
|
||||
description: AirOSBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the binary sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
@@ -6,11 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
KeyDataMissingError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
ConnectionSetupError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (ConnectionAuthenticationError, DataMissingError):
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except KeyDataMissingError:
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
@@ -6,10 +6,10 @@ import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (ConnectionAuthenticationError,) as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (DataMissingError,) as err:
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.1"]
|
||||
"requirements": ["airos==0.2.7"]
|
||||
}
|
||||
|
@@ -54,9 +54,7 @@ rules:
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: todo
|
||||
comment: prepared binary_sensors will provide this
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
|
@@ -46,6 +46,7 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="host_cpuload",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda data: data.host.cpuload,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -69,13 +70,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
@@ -90,6 +84,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.throughput.tx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -98,6 +94,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.throughput.rx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -106,6 +104,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.polling.dl_capacity,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -114,6 +114,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.polling.ul_capacity,
|
||||
),
|
||||
)
|
||||
|
@@ -26,6 +26,23 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"port_forwarding": {
|
||||
"name": "Port forwarding"
|
||||
},
|
||||
"dhcp_client": {
|
||||
"name": "DHCP client"
|
||||
},
|
||||
"dhcp_server": {
|
||||
"name": "DHCP server"
|
||||
},
|
||||
"dhcp6_server": {
|
||||
"name": "DHCPv6 server"
|
||||
},
|
||||
"pppoe": {
|
||||
"name": "PPPoE link"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"host_cpuload": {
|
||||
"name": "CPU load"
|
||||
@@ -43,13 +60,6 @@
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"ap_ptp": "Access point",
|
||||
"sta_ptp": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
|
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
@@ -9,7 +9,6 @@ DOMAIN: Final = "amberelectric"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_CHANNEL_TYPE = "channel_type"
|
||||
|
||||
ATTRIBUTION = "Data provided by Amber Electric"
|
||||
|
@@ -4,6 +4,7 @@ from amberelectric.models.channel import ChannelType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
@@ -16,7 +17,6 @@ from homeassistant.util.json import JsonValueType
|
||||
|
||||
from .const import (
|
||||
ATTR_CHANNEL_TYPE,
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONTROLLED_LOAD_CHANNEL,
|
||||
DOMAIN,
|
||||
FEED_IN_CHANNEL,
|
||||
|
@@ -81,11 +81,15 @@ async def async_update_options(
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
# Make sure we get enabled config entries first
|
||||
entries = sorted(
|
||||
hass.config_entries.async_entries(DOMAIN),
|
||||
key=lambda e: e.disabled_by is not None,
|
||||
)
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
)
|
||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||
all_disabled = all(
|
||||
e.disabled_by is not None
|
||||
for e in entries
|
||||
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||
)
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
|
||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||
conversation_entity = entity_registry.async_get_entity_id(
|
||||
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||
"conversation",
|
||||
DOMAIN,
|
||||
entry.entry_id,
|
||||
)
|
||||
if conversation_entity is not None:
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, entry.entry_id)}
|
||||
)
|
||||
|
||||
if conversation_entity_id is not None:
|
||||
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||
if (
|
||||
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
# Device and entity registries don't update the disabled_by flag
|
||||
# when moving a device or entity from one config entry to another,
|
||||
# so we need to do it manually.
|
||||
entity_disabled_by = (
|
||||
er.RegistryEntryDisabler.DEVICE
|
||||
if device
|
||||
else er.RegistryEntryDisabler.USER
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity_id,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
disabled_by=entity_disabled_by,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
if device is not None:
|
||||
# Device and entity registries don't update the disabled_by flag when
|
||||
# moving a device or entity from one config entry to another, so we
|
||||
# need to do it manually.
|
||||
device_disabled_by = device.disabled_by
|
||||
if (
|
||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=device_disabled_by,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=DEFAULT_CONVERSATION_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
|
||||
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
# Fix migration where the disabled_by flag was not set correctly.
|
||||
# We can currently only correct this for enabled config entries,
|
||||
# because migration does not run for disabled config entries. This
|
||||
# is asserted in tests, and if that behavior is changed, we should
|
||||
# correct also disabled config entries.
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
if entry.disabled_by is None:
|
||||
# If the config entry is not disabled, we need to set the disabled_by
|
||||
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||
# to CONFIG_ENTRY.
|
||||
for device in devices:
|
||||
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||
)
|
||||
for entity in entity_entries:
|
||||
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
entity_registry.async_update_entity(
|
||||
entity.entity_id,
|
||||
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@@ -20,10 +20,8 @@ RECOMMENDED_THINKING_BUDGET = 0
|
||||
MIN_THINKING_BUDGET = 1024
|
||||
|
||||
THINKING_MODELS = [
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-3-7-sonnet-latest",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3-7-sonnet",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-1",
|
||||
]
|
||||
|
@@ -361,7 +361,10 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
"system": system.content,
|
||||
"stream": True,
|
||||
}
|
||||
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
|
||||
if (
|
||||
model.startswith(tuple(THINKING_MODELS))
|
||||
and thinking_budget >= MIN_THINKING_BUDGET
|
||||
):
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
)
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.52.0"]
|
||||
"requirements": ["anthropic==0.62.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["apcaccess"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioapcaccess==0.4.2"]
|
||||
}
|
||||
|
98
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
98
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
@@ -0,0 +1,98 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules:
|
||||
status: done
|
||||
comment: |
|
||||
Consider deriving a base entity.
|
||||
config-flow-test-coverage:
|
||||
status: done
|
||||
comment: |
|
||||
Consider looking into making a `mock_setup_entry` fixture that just automatically do this.
|
||||
`test_config_flow_cannot_connect`: Needs to end in CREATE_ENTRY to test that its able to recover.
|
||||
`test_config_flow_duplicate`: this test should be split in 2, one for testing duplicate host/port and one for duplicate serial number.
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional options.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not require authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Patch `aioapcaccess.request_status` where we use it.
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration connects to a single service per configuration entry.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration connect to a single service per configuration entry.
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not connect via HTTP.
|
||||
strict-typing: done
|
@@ -14,7 +14,22 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the APC UPS Daemon",
|
||||
"port": "The port the APC UPS Daemon is listening on"
|
||||
},
|
||||
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
|
||||
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
|
||||
},
|
||||
"description": "[%key:component::apcupsd::config::step::user::description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
|
||||
@@ -40,6 +40,9 @@ from .const import (
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AsusWrtConfigEntry
|
||||
|
||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AsusWrtSensorDataHandler:
|
||||
"""Data handler for AsusWrt sensor."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||
) -> None:
|
||||
"""Initialize a AsusWrt sensor data handler."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._entry = entry
|
||||
self._connected_devices = 0
|
||||
|
||||
async def _get_connected_devices(self) -> dict[str, int]:
|
||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
||||
update_method=method,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||
config_entry=self._entry,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
||||
if self._sensors_data_handler:
|
||||
return
|
||||
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||
self.hass, self._api, self._entry
|
||||
)
|
||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||
|
||||
sensors_types = await self._api.async_get_available_sensors()
|
||||
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
|
||||
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
API_ABS_HUMID = "abs_humid"
|
||||
API_CO2 = "carbon_dioxide"
|
||||
API_DEW_POINT = "dew_point"
|
||||
API_DUST = "dust"
|
||||
|
@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_SW_VERSION,
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
@@ -33,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
API_ABS_HUMID,
|
||||
API_CO2,
|
||||
API_DEW_POINT,
|
||||
API_DUST,
|
||||
@@ -120,6 +122,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AwairSensorEntityDescription(
|
||||
key=API_ABS_HUMID,
|
||||
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
|
||||
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
unique_id_tag="absolute_humidity",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
|
||||
|
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==64"],
|
||||
"requirements": ["axis==65"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
@@ -127,7 +127,6 @@ class BackupConfigData:
|
||||
schedule=BackupSchedule(
|
||||
days=days,
|
||||
recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]),
|
||||
state=ScheduleState(data["schedule"].get("state", ScheduleState.NEVER)),
|
||||
time=time,
|
||||
),
|
||||
)
|
||||
@@ -453,7 +452,6 @@ class StoredBackupSchedule(TypedDict):
|
||||
|
||||
days: list[Day]
|
||||
recurrence: ScheduleRecurrence
|
||||
state: ScheduleState
|
||||
time: str | None
|
||||
|
||||
|
||||
@@ -462,7 +460,6 @@ class ScheduleParametersDict(TypedDict, total=False):
|
||||
|
||||
days: list[Day]
|
||||
recurrence: ScheduleRecurrence
|
||||
state: ScheduleState
|
||||
time: dt.time | None
|
||||
|
||||
|
||||
@@ -486,32 +483,12 @@ class ScheduleRecurrence(StrEnum):
|
||||
CUSTOM_DAYS = "custom_days"
|
||||
|
||||
|
||||
class ScheduleState(StrEnum):
|
||||
"""Represent the schedule recurrence.
|
||||
|
||||
This is deprecated and can be remove in HA Core 2025.8.
|
||||
"""
|
||||
|
||||
NEVER = "never"
|
||||
DAILY = "daily"
|
||||
MONDAY = "mon"
|
||||
TUESDAY = "tue"
|
||||
WEDNESDAY = "wed"
|
||||
THURSDAY = "thu"
|
||||
FRIDAY = "fri"
|
||||
SATURDAY = "sat"
|
||||
SUNDAY = "sun"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BackupSchedule:
|
||||
"""Represent the backup schedule."""
|
||||
|
||||
days: list[Day] = field(default_factory=list)
|
||||
recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER
|
||||
# Although no longer used, state is kept for backwards compatibility.
|
||||
# It can be removed in HA Core 2025.8.
|
||||
state: ScheduleState = ScheduleState.NEVER
|
||||
time: dt.time | None = None
|
||||
cron_event: CronSim | None = field(init=False, default=None)
|
||||
next_automatic_backup: datetime | None = field(init=False, default=None)
|
||||
@@ -610,7 +587,6 @@ class BackupSchedule:
|
||||
return StoredBackupSchedule(
|
||||
days=self.days,
|
||||
recurrence=self.recurrence,
|
||||
state=self.state,
|
||||
time=self.time.isoformat() if self.time else None,
|
||||
)
|
||||
|
||||
|
@@ -331,9 +331,6 @@ async def handle_config_info(
|
||||
"""Send the stored backup config."""
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
config = manager.config.data.to_dict()
|
||||
# Remove state from schedule, it's not needed in the frontend
|
||||
# mypy doesn't like deleting from TypedDict, ignore it
|
||||
del config["schedule"]["state"] # type: ignore[misc]
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
|
@@ -25,7 +25,6 @@ SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
@@ -5,12 +5,12 @@ from __future__ import annotations
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_PIN
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
||||
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkConfigEntry
|
||||
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
|
@@ -388,12 +388,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||
scanner = HaScanner(mode, adapter, address)
|
||||
scanner.async_setup()
|
||||
try:
|
||||
await scanner.async_start()
|
||||
except (RuntimeError, ScannerStartError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{adapter_human_name(adapter, address)}: {err}"
|
||||
) from err
|
||||
adapters = await manager.async_get_bluetooth_adapters()
|
||||
details = adapters[adapter]
|
||||
if entry.title == address:
|
||||
@@ -401,8 +395,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, title=adapter_title(adapter, details)
|
||||
)
|
||||
slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS
|
||||
# Register the scanner before starting so
|
||||
# any raw advertisement data can be processed
|
||||
entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots))
|
||||
await async_update_device(hass, entry, adapter, details)
|
||||
try:
|
||||
await scanner.async_start()
|
||||
except (RuntimeError, ScannerStartError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{adapter_human_name(adapter, address)}: {err}"
|
||||
) from err
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_listener))
|
||||
entry.async_on_unload(scanner.async_stop)
|
||||
return True
|
||||
|
@@ -235,10 +235,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
|
||||
def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
|
||||
"""Save the scanner history."""
|
||||
if isinstance(scanner, BaseHaRemoteScanner):
|
||||
self.storage.async_set_advertisement_history(
|
||||
scanner.source, scanner.serialize_discovered_devices()
|
||||
)
|
||||
self.storage.async_set_advertisement_history(
|
||||
scanner.source, scanner.serialize_discovered_devices()
|
||||
)
|
||||
|
||||
def _async_unregister_scanner(
|
||||
self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
|
||||
@@ -285,9 +284,8 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
connection_slots: int | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register a scanner."""
|
||||
if isinstance(scanner, BaseHaRemoteScanner):
|
||||
if history := self.storage.async_get_advertisement_history(scanner.source):
|
||||
scanner.restore_discovered_devices(history)
|
||||
if history := self.storage.async_get_advertisement_history(scanner.source):
|
||||
scanner.restore_discovered_devices(history)
|
||||
|
||||
unregister = super().async_register_scanner(scanner, connection_slots)
|
||||
return partial(self._async_unregister_scanner, scanner, unregister)
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.44.2",
|
||||
"habluetooth==4.0.1"
|
||||
"dbus-fast==2.44.3",
|
||||
"habluetooth==5.0.1"
|
||||
]
|
||||
}
|
||||
|
@@ -39,7 +39,13 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
def serialize_service_info(
|
||||
service_info: BluetoothServiceInfoBleak, time_diff: float
|
||||
) -> dict[str, Any]:
|
||||
"""Serialize a BluetoothServiceInfoBleak object."""
|
||||
"""Serialize a BluetoothServiceInfoBleak object.
|
||||
|
||||
The raw field is included for:
|
||||
1. Debugging - to see the actual advertisement packet
|
||||
2. Data freshness - manufacturer_data and service_data are aggregated
|
||||
across multiple advertisements, raw shows the latest packet only
|
||||
"""
|
||||
return {
|
||||
"name": service_info.name,
|
||||
"address": service_info.address,
|
||||
@@ -57,6 +63,7 @@ def serialize_service_info(
|
||||
"connectable": service_info.connectable,
|
||||
"time": service_info.time + time_diff,
|
||||
"tx_power": service_info.tx_power,
|
||||
"raw": service_info.raw.hex() if service_info.raw else None,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -6,4 +6,3 @@ CONF_INSTALLER_CODE = "installer_code"
|
||||
CONF_USER_CODE = "user_code"
|
||||
ATTR_DATETIME = "datetime"
|
||||
SERVICE_SET_DATE_TIME = "set_date_time"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
@@ -9,12 +9,13 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||
from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
|
||||
|
@@ -95,7 +95,7 @@
|
||||
"name": "Battery missing"
|
||||
},
|
||||
"panel_fault_ac_fail": {
|
||||
"name": "AC Failure"
|
||||
"name": "AC failure"
|
||||
},
|
||||
"panel_fault_parameter_crc_fail_in_pif": {
|
||||
"name": "CRC failure in panel configuration"
|
||||
|
@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
|
||||
device.hass,
|
||||
_LOGGER,
|
||||
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
|
||||
config_entry=device.config,
|
||||
update_method=self.async_update,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -2,7 +2,16 @@
|
||||
|
||||
import dataclasses
|
||||
|
||||
from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState
|
||||
from bsblan import (
|
||||
BSBLAN,
|
||||
BSBLANAuthError,
|
||||
BSBLANConfig,
|
||||
BSBLANConnectionError,
|
||||
BSBLANError,
|
||||
Device,
|
||||
Info,
|
||||
StaticState,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -13,9 +22,14 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_PASSKEY
|
||||
from .const import CONF_PASSKEY, DOMAIN
|
||||
from .coordinator import BSBLanUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
@@ -54,10 +68,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Fetch all required data concurrently
|
||||
device = await bsblan.device()
|
||||
info = await bsblan.info()
|
||||
static = await bsblan.static_values()
|
||||
try:
|
||||
# Fetch all required data sequentially
|
||||
device = await bsblan.device()
|
||||
info = await bsblan.info()
|
||||
static = await bsblan.static_values()
|
||||
except BSBLANConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_connection_error",
|
||||
translation_placeholders={"host": entry.data[CONF_HOST]},
|
||||
) from err
|
||||
except BSBLANAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_auth_error",
|
||||
) from err
|
||||
except BSBLANError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_general_error",
|
||||
) from err
|
||||
|
||||
entry.runtime_data = BSBLanData(
|
||||
client=bsblan,
|
||||
|
@@ -211,16 +211,16 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
)
|
||||
|
||||
# Use existing host and port, update auth credentials
|
||||
self.host = existing_entry.data[CONF_HOST]
|
||||
self.port = existing_entry.data[CONF_PORT]
|
||||
self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get(
|
||||
CONF_PASSKEY
|
||||
)
|
||||
self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get(
|
||||
CONF_USERNAME
|
||||
)
|
||||
self.password = user_input.get(CONF_PASSWORD)
|
||||
# Combine existing data with the user's new input for validation.
|
||||
# This correctly handles adding, changing, and clearing credentials.
|
||||
config_data = existing_entry.data.copy()
|
||||
config_data.update(user_input)
|
||||
|
||||
self.host = config_data[CONF_HOST]
|
||||
self.port = config_data[CONF_PORT]
|
||||
self.passkey = config_data.get(CONF_PASSKEY)
|
||||
self.username = config_data.get(CONF_USERNAME)
|
||||
self.password = config_data.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
|
||||
@@ -267,17 +267,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors={"base": "cannot_connect"},
|
||||
)
|
||||
|
||||
# Update the config entry with new auth data
|
||||
data_updates = {}
|
||||
if self.passkey is not None:
|
||||
data_updates[CONF_PASSKEY] = self.passkey
|
||||
if self.username is not None:
|
||||
data_updates[CONF_USERNAME] = self.username
|
||||
if self.password is not None:
|
||||
data_updates[CONF_PASSWORD] = self.password
|
||||
|
||||
# Update only the fields that were provided by the user
|
||||
return self.async_update_reload_and_abort(
|
||||
existing_entry, data_updates=data_updates, reason="reauth_successful"
|
||||
existing_entry, data_updates=user_input, reason="reauth_successful"
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@@ -41,6 +41,11 @@
|
||||
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
|
||||
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -66,6 +71,15 @@
|
||||
},
|
||||
"set_operation_mode_error": {
|
||||
"message": "An error occurred while setting the operation mode"
|
||||
},
|
||||
"setup_connection_error": {
|
||||
"message": "Failed to retrieve static device data from BSB-Lan device at {host}"
|
||||
},
|
||||
"setup_auth_error": {
|
||||
"message": "Authentication failed while retrieving static device data"
|
||||
},
|
||||
"setup_general_error": {
|
||||
"message": "An unknown error occurred while retrieving static device data"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"services": {
|
||||
"press": {
|
||||
"name": "Press",
|
||||
"description": "Press the button entity."
|
||||
"description": "Presses a button entity."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
@@ -100,16 +100,10 @@ set_hvac_mode:
|
||||
fields:
|
||||
hvac_mode:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "off"
|
||||
- "auto"
|
||||
- "cool"
|
||||
- "dry"
|
||||
- "fan_only"
|
||||
- "heat_cool"
|
||||
- "heat"
|
||||
translation_key: hvac_mode
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
set_swing_mode:
|
||||
target:
|
||||
entity:
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.110.0"],
|
||||
"requirements": ["hass-nabucasa==0.111.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -4,11 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo
|
||||
from hass_nabucasa import (
|
||||
Cloud,
|
||||
MigratePaypalAgreementInfo,
|
||||
PaymentsApiError,
|
||||
SubscriptionInfo,
|
||||
)
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import REQUEST_TIMEOUT
|
||||
@@ -29,17 +31,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
|
||||
|
||||
async def async_migrate_paypal_agreement(
|
||||
cloud: Cloud[CloudClient],
|
||||
) -> dict[str, Any] | None:
|
||||
) -> MigratePaypalAgreementInfo | None:
|
||||
"""Migrate a paypal agreement from legacy."""
|
||||
try:
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
return await cloud_api.async_migrate_paypal_agreement(cloud)
|
||||
return await cloud.payments.migrate_paypal_agreement()
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"A timeout of %s was reached while trying to start agreement migration",
|
||||
REQUEST_TIMEOUT,
|
||||
)
|
||||
except ClientError as exception:
|
||||
except PaymentsApiError as exception:
|
||||
_LOGGER.error("Failed to start agreement migration - %s", exception)
|
||||
|
||||
return None
|
||||
|
@@ -161,7 +161,9 @@ class AssistantContent:
|
||||
role: Literal["assistant"] = field(init=False, default="assistant")
|
||||
agent_id: str
|
||||
content: str | None = None
|
||||
thinking_content: str | None = None
|
||||
tool_calls: list[llm.ToolInput] | None = None
|
||||
native: Any = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -183,7 +185,9 @@ class AssistantContentDeltaDict(TypedDict, total=False):
|
||||
|
||||
role: Literal["assistant"]
|
||||
content: str | None
|
||||
thinking_content: str | None
|
||||
tool_calls: list[llm.ToolInput] | None
|
||||
native: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -306,6 +310,8 @@ class ChatLog:
|
||||
The keys content and tool_calls will be concatenated if they appear multiple times.
|
||||
"""
|
||||
current_content = ""
|
||||
current_thinking_content = ""
|
||||
current_native: Any = None
|
||||
current_tool_calls: list[llm.ToolInput] = []
|
||||
tool_call_tasks: dict[str, asyncio.Task] = {}
|
||||
|
||||
@@ -316,6 +322,14 @@ class ChatLog:
|
||||
if "role" not in delta:
|
||||
if delta_content := delta.get("content"):
|
||||
current_content += delta_content
|
||||
if delta_thinking_content := delta.get("thinking_content"):
|
||||
current_thinking_content += delta_thinking_content
|
||||
if delta_native := delta.get("native"):
|
||||
if current_native is not None:
|
||||
raise RuntimeError(
|
||||
"Native content already set, cannot overwrite"
|
||||
)
|
||||
current_native = delta_native
|
||||
if delta_tool_calls := delta.get("tool_calls"):
|
||||
if self.llm_api is None:
|
||||
raise ValueError("No LLM API configured")
|
||||
@@ -337,11 +351,18 @@ class ChatLog:
|
||||
raise ValueError(f"Only assistant role expected. Got {delta['role']}")
|
||||
|
||||
# Yield the previous message if it has content
|
||||
if current_content or current_tool_calls:
|
||||
if (
|
||||
current_content
|
||||
or current_thinking_content
|
||||
or current_tool_calls
|
||||
or current_native
|
||||
):
|
||||
content = AssistantContent(
|
||||
agent_id=agent_id,
|
||||
content=current_content or None,
|
||||
thinking_content=current_thinking_content or None,
|
||||
tool_calls=current_tool_calls or None,
|
||||
native=current_native,
|
||||
)
|
||||
yield content
|
||||
async for tool_result in self.async_add_assistant_content(
|
||||
@@ -352,16 +373,25 @@ class ChatLog:
|
||||
self.delta_listener(self, asdict(tool_result))
|
||||
|
||||
current_content = delta.get("content") or ""
|
||||
current_thinking_content = delta.get("thinking_content") or ""
|
||||
current_tool_calls = delta.get("tool_calls") or []
|
||||
current_native = delta.get("native")
|
||||
|
||||
if self.delta_listener:
|
||||
self.delta_listener(self, delta) # type: ignore[arg-type]
|
||||
|
||||
if current_content or current_tool_calls:
|
||||
if (
|
||||
current_content
|
||||
or current_thinking_content
|
||||
or current_tool_calls
|
||||
or current_native
|
||||
):
|
||||
content = AssistantContent(
|
||||
agent_id=agent_id,
|
||||
content=current_content or None,
|
||||
thinking_content=current_thinking_content or None,
|
||||
tool_calls=current_tool_calls or None,
|
||||
native=current_native,
|
||||
)
|
||||
yield content
|
||||
async for tool_result in self.async_add_assistant_content(
|
||||
|
@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
|
||||
self._hass = hass
|
||||
|
||||
@classmethod
|
||||
async def async_validate_condition_config(
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
@@ -69,7 +69,7 @@ class DeviceCondition(Condition):
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
|
||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"device": DeviceCondition,
|
||||
"_device": DeviceCondition,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# If path is relative, we assume relative to Home Assistant config dir
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
|
||||
)
|
||||
|
||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
|
@@ -11,6 +11,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
|
||||
|
||||
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
||||
url: str = service.data[ATTR_URL]
|
||||
subdir: str | None = service.data.get(ATTR_SUBDIR)
|
||||
target_filename: str | None = service.data.get(ATTR_FILENAME)
|
||||
overwrite: bool = service.data[ATTR_OVERWRITE]
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
try:
|
||||
raise_if_invalid_path(subdir)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_invalid",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
) from err
|
||||
if os.path.isabs(subdir):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_not_relative",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
)
|
||||
|
||||
def do_download() -> None:
|
||||
"""Download the file."""
|
||||
final_path = None
|
||||
filename = target_filename
|
||||
try:
|
||||
url = service.data[ATTR_URL]
|
||||
|
||||
subdir = service.data.get(ATTR_SUBDIR)
|
||||
|
||||
filename = service.data.get(ATTR_FILENAME)
|
||||
|
||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
raise_if_invalid_path(subdir)
|
||||
|
||||
final_path = None
|
||||
|
||||
req = requests.get(url, stream=True, timeout=10)
|
||||
|
||||
if req.status_code != HTTPStatus.OK:
|
||||
|
@@ -12,6 +12,14 @@
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"subdir_invalid": {
|
||||
"message": "Invalid subdirectory, got: {subdir}"
|
||||
},
|
||||
"subdir_not_relative": {
|
||||
"message": "Subdirectory must be relative, got: {subdir}"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"download_file": {
|
||||
"name": "Download file",
|
||||
|
@@ -20,7 +20,6 @@ from homeassistant.const import Platform
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
ATTR_CONFIG_ENTRY_ID = "entry_id"
|
||||
ATTR_AVAILABLE_SENSORS = "available_sensors"
|
||||
ATTR_ACTIVE_SENSORS = "active_sensors"
|
||||
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
CONF_EXCLUDE_FEEDID = "exclude_feed_id"
|
||||
CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
|
||||
CONF_MESSAGE = "message"
|
||||
CONF_SUCCESS = "success"
|
||||
|
@@ -34,13 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .config_flow import sensor_name
|
||||
from .const import (
|
||||
CONF_EXCLUDE_FEEDID,
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
FEED_ID,
|
||||
FEED_NAME,
|
||||
FEED_TAG,
|
||||
)
|
||||
from .const import CONF_ONLY_INCLUDE_FEEDID, FEED_ID, FEED_NAME, FEED_TAG
|
||||
from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator
|
||||
|
||||
SENSORS: dict[str | None, SensorEntityDescription] = {
|
||||
@@ -200,12 +194,11 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the emoncms sensors."""
|
||||
name = sensor_name(entry.data[CONF_URL])
|
||||
exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID)
|
||||
include_only_feeds = entry.options.get(
|
||||
CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
)
|
||||
|
||||
if exclude_feeds is None and include_only_feeds is None:
|
||||
if include_only_feeds is None:
|
||||
return
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
@@ -111,14 +111,6 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"remove_value_template": {
|
||||
"title": "The {domain} integration cannot start",
|
||||
"description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually."
|
||||
},
|
||||
"missing_include_only_feed_id": {
|
||||
"title": "No feed synchronized with the {domain} sensor",
|
||||
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
|
||||
},
|
||||
"migrate_database": {
|
||||
"title": "Upgrade your emoncms version",
|
||||
"description": "Your [emoncms]({url}) does not ship a unique identifier.\n\nPlease upgrade to at least version 11.5.7 and migrate your emoncms database.\n\nMore info in the [emoncms documentation]({doc_url})"
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Data update coordinator for the Enigma2 integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
|
||||
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
SETUP_TIMEOUT = 10
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Provide needed data to the device info."""
|
||||
|
||||
about = await self.device.get_about()
|
||||
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
|
||||
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
|
||||
self.device_info["model"] = about["info"]["model"]
|
||||
self.device_info["manufacturer"] = about["info"]["brand"]
|
||||
|
@@ -117,6 +117,7 @@ class FreeboxRouter:
|
||||
self.name: str = freebox_config["model_info"]["pretty_name"]
|
||||
self.mac: str = freebox_config["mac"]
|
||||
self._sw_v: str = freebox_config["firmware_version"]
|
||||
self._hw_v: str | None = freebox_config.get("board_name")
|
||||
self._attrs: dict[str, Any] = {}
|
||||
|
||||
self.supports_hosts = True
|
||||
@@ -282,7 +283,9 @@ class FreeboxRouter:
|
||||
identifiers={(DOMAIN, self.mac)},
|
||||
manufacturer="Freebox SAS",
|
||||
name=self.name,
|
||||
model=self.name,
|
||||
sw_version=self._sw_v,
|
||||
hw_version=self._hw_v,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -106,6 +106,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_logger_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
await self.logger_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -120,6 +121,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_meters_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -129,6 +131,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_ohmpilot_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -138,6 +141,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_power_flow_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,6 +151,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_storages_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +211,7 @@ class FroniusSolarNet:
|
||||
logger=_LOGGER,
|
||||
name=_inverter_name,
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250731.0"]
|
||||
"requirements": ["home-assistant-frontend==20250806.0"]
|
||||
}
|
||||
|
@@ -124,7 +124,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
|
||||
if not response.candidates[0].content.parts:
|
||||
if (
|
||||
not response.candidates
|
||||
or not response.candidates[0].content
|
||||
or not response.candidates[0].content.parts
|
||||
):
|
||||
raise HomeAssistantError("Unknown error generating content")
|
||||
|
||||
return {"text": response.text}
|
||||
|
@@ -377,7 +377,7 @@ async def google_generative_ai_config_option_schema(
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(
|
||||
api_models, key=lambda x: x.name.lstrip("models/") or ""
|
||||
api_models, key=lambda x: (x.name or "").lstrip("models/")
|
||||
)
|
||||
if (
|
||||
api_model.name
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import codecs
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from collections.abc import AsyncGenerator, AsyncIterator, Callable
|
||||
from dataclasses import replace
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
@@ -15,6 +15,7 @@ from google.genai.errors import APIError, ClientError
|
||||
from google.genai.types import (
|
||||
AutomaticFunctionCallingConfig,
|
||||
Content,
|
||||
ContentDict,
|
||||
File,
|
||||
FileState,
|
||||
FunctionDeclaration,
|
||||
@@ -23,9 +24,11 @@ from google.genai.types import (
|
||||
GoogleSearch,
|
||||
HarmCategory,
|
||||
Part,
|
||||
PartUnionDict,
|
||||
SafetySetting,
|
||||
Schema,
|
||||
Tool,
|
||||
ToolListUnion,
|
||||
)
|
||||
import voluptuous as vol
|
||||
from voluptuous_openapi import convert
|
||||
@@ -237,7 +240,7 @@ def _convert_content(
|
||||
|
||||
|
||||
async def _transform_stream(
|
||||
result: AsyncGenerator[GenerateContentResponse],
|
||||
result: AsyncIterator[GenerateContentResponse],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
new_message = True
|
||||
try:
|
||||
@@ -342,7 +345,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.subentry.data
|
||||
|
||||
tools: list[Tool | Callable[..., Any]] | None = None
|
||||
tools: ToolListUnion | None = None
|
||||
if chat_log.llm_api:
|
||||
tools = [
|
||||
_format_tool(tool, chat_log.llm_api.custom_serializer)
|
||||
@@ -373,7 +376,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
else:
|
||||
raise HomeAssistantError("Invalid prompt content")
|
||||
|
||||
messages: list[Content] = []
|
||||
messages: list[Content | ContentDict] = []
|
||||
|
||||
# Google groups tool results, we do not. Group them before sending.
|
||||
tool_results: list[conversation.ToolResultContent] = []
|
||||
@@ -400,7 +403,10 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
# The SDK requires the first message to be a user message
|
||||
# This is not the case if user used `start_conversation`
|
||||
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
|
||||
if messages and messages[0].role != "user":
|
||||
if messages and (
|
||||
(isinstance(messages[0], Content) and messages[0].role != "user")
|
||||
or (isinstance(messages[0], dict) and messages[0]["role"] != "user")
|
||||
):
|
||||
messages.insert(
|
||||
0,
|
||||
Content(role="user", parts=[Part.from_text(text=" ")]),
|
||||
@@ -440,14 +446,14 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
)
|
||||
user_message = chat_log.content[-1]
|
||||
assert isinstance(user_message, conversation.UserContent)
|
||||
chat_request: str | list[Part] = user_message.content
|
||||
chat_request: list[PartUnionDict] = [user_message.content]
|
||||
if user_message.attachments:
|
||||
files = await async_prepare_files_for_prompt(
|
||||
self.hass,
|
||||
self._genai_client,
|
||||
[a.path for a in user_message.attachments],
|
||||
)
|
||||
chat_request = [chat_request, *files]
|
||||
chat_request = [*chat_request, *files]
|
||||
|
||||
# To prevent infinite loops, we limit the number of iterations
|
||||
for _iteration in range(MAX_TOOL_ITERATIONS):
|
||||
@@ -464,15 +470,17 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
error = ERROR_GETTING_RESPONSE
|
||||
raise HomeAssistantError(error) from err
|
||||
|
||||
chat_request = _create_google_tool_response_parts(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(chat_response_generator),
|
||||
)
|
||||
if isinstance(content, conversation.ToolResultContent)
|
||||
]
|
||||
chat_request = list(
|
||||
_create_google_tool_response_parts(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(chat_response_generator),
|
||||
)
|
||||
if isinstance(content, conversation.ToolResultContent)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
@@ -559,13 +567,13 @@ async def async_prepare_files_for_prompt(
|
||||
await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS)
|
||||
|
||||
uploaded_file = await client.aio.files.get(
|
||||
name=uploaded_file.name,
|
||||
name=uploaded_file.name or "",
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}"
|
||||
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message if uploaded_file.error else 'unknown'}"
|
||||
)
|
||||
|
||||
prompt_parts = await hass.async_add_executor_job(upload_files)
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["google-genai==1.7.0"]
|
||||
"requirements": ["google-genai==1.29.0"]
|
||||
}
|
||||
|
@@ -123,10 +123,10 @@
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
"user": "Add AI task",
|
||||
"reconfigure": "Reconfigure AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
|
@@ -146,15 +146,41 @@ class GoogleGenerativeAITextToSpeechEntity(
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def _extract_audio_parts(
|
||||
response: types.GenerateContentResponse,
|
||||
) -> tuple[bytes, str]:
|
||||
if (
|
||||
not response.candidates
|
||||
or not response.candidates[0].content
|
||||
or not response.candidates[0].content.parts
|
||||
or not response.candidates[0].content.parts[0].inline_data
|
||||
):
|
||||
raise ValueError("No content returned from TTS generation")
|
||||
|
||||
data = response.candidates[0].content.parts[0].inline_data.data
|
||||
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError(
|
||||
f"Expected bytes for audio data, got {type(data).__name__}"
|
||||
)
|
||||
if not isinstance(mime_type, str):
|
||||
raise TypeError(
|
||||
f"Expected str for mime_type, got {type(mime_type).__name__}"
|
||||
)
|
||||
|
||||
return data, mime_type
|
||||
|
||||
try:
|
||||
response = await self._genai_client.aio.models.generate_content(
|
||||
model=self.subentry.data.get(CONF_CHAT_MODEL, RECOMMENDED_TTS_MODEL),
|
||||
contents=message,
|
||||
config=config,
|
||||
)
|
||||
data = response.candidates[0].content.parts[0].inline_data.data
|
||||
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
|
||||
except (APIError, ClientError, ValueError) as exc:
|
||||
|
||||
data, mime_type = _extract_audio_parts(response)
|
||||
except (APIError, ClientError, ValueError, TypeError) as exc:
|
||||
LOGGER.error("Error during TTS: %s", exc, exc_info=True)
|
||||
raise HomeAssistantError(exc) from exc
|
||||
return "wav", convert_to_wav(data, mime_type)
|
||||
|
@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"duplicate_os_installation",
|
||||
"oserror_bad_message",
|
||||
"privileged",
|
||||
"setup",
|
||||
"supervisor",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
@@ -117,35 +117,43 @@
|
||||
},
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"title": "Unhealthy system - Docker misconfigured",
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
|
||||
"unhealthy_duplicate_os_installation": {
|
||||
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
|
||||
"unhealthy_oserror_bad_message": {
|
||||
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Operating System error: Bad message"
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"title": "Unhealthy system - Not privileged",
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"title": "Unhealthy system - Untrusted code",
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Unsupported system - {reason}",
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"title": "Unsupported system - AppArmor issues",
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"title": "Unsupported system - CGroup version",
|
||||
@@ -153,23 +161,23 @@
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"title": "Unsupported system - Connectivity check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"title": "Unsupported system - Content-trust check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"title": "Unsupported system - D-Bus issues",
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"title": "Unsupported system - DNS server issues",
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"title": "Unsupported system - Docker misconfigured",
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"title": "Unsupported system - Docker version",
|
||||
@@ -177,15 +185,15 @@
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"title": "Unsupported system - Protections disabled",
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"title": "Unsupported system - LXC detected",
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"title": "Unsupported system - Network Manager issues",
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_os": {
|
||||
"title": "Unsupported system - Operating System",
|
||||
@@ -193,39 +201,43 @@
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"title": "Unsupported system - OS-Agent issues",
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"title": "Unsupported system - Container restart policy",
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_software": {
|
||||
"title": "Unsupported system - Unsupported software",
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"title": "Unsupported system - Supervisor source modifications",
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"title": "Unsupported system - Supervisor version",
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"title": "Unsupported system - Systemd issues",
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"title": "Unsupported system - Systemd Journal issues",
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"title": "Unsupported system - Systemd-Resolved issues",
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_virtualization_image": {
|
||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_os_version": {
|
||||
"title": "Unsupported system - Home Assistant OS version",
|
||||
"description": "System is unsupported because the Home Assistant OS version in use is not supported. For troubleshooting information, select Learn more."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.77", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.78", "babel==2.15.0"]
|
||||
}
|
||||
|
@@ -11,10 +11,16 @@ from pyHomee import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
RESULT_CANNOT_CONNECT,
|
||||
RESULT_INVALID_AUTH,
|
||||
RESULT_UNKNOWN_ERROR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,60 +39,137 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
homee: Homee
|
||||
_host: str
|
||||
_name: str
|
||||
_reauth_host: str
|
||||
_reauth_username: str
|
||||
|
||||
async def _connect_homee(self) -> dict[str, str]:
|
||||
errors: dict[str, str] = {}
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
_LOGGER.info("Got access token for homee")
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
_LOGGER.debug("Homee task created")
|
||||
await self.homee.wait_until_connected()
|
||||
_LOGGER.info("Homee connected")
|
||||
self.homee.disconnect()
|
||||
_LOGGER.debug("Homee disconnecting")
|
||||
await self.homee.wait_until_disconnected()
|
||||
_LOGGER.info("Homee config successfully tested")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid)
|
||||
|
||||
return errors
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial user step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
errors = await self._connect_homee()
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
_LOGGER.info("Got access token for homee")
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
_LOGGER.debug("Homee task created")
|
||||
await self.homee.wait_until_connected()
|
||||
_LOGGER.info("Homee connected")
|
||||
self.homee.disconnect()
|
||||
_LOGGER.debug("Homee disconnecting")
|
||||
await self.homee.wait_until_disconnected()
|
||||
_LOGGER.info("Homee config successfully tested")
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.info(
|
||||
"Created new homee entry with ID %s", self.homee.settings.uid
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
|
||||
# Ensure that an IPv4 address is received
|
||||
self._host = discovery_info.host
|
||||
self._name = discovery_info.hostname[6:18]
|
||||
if discovery_info.ip_address.version == 6:
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
await self.async_set_unique_id(self._name)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
|
||||
|
||||
# Cause an auth-error to see if homee is reachable.
|
||||
self.homee = Homee(
|
||||
self._host,
|
||||
"dummy_username",
|
||||
"dummy_password",
|
||||
)
|
||||
errors = await self._connect_homee()
|
||||
if errors["base"] != RESULT_INVALID_AUTH:
|
||||
return self.async_abort(reason=RESULT_CANNOT_CONNECT)
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._name, "host": self._host}
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the configuration of the device."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self.homee = Homee(
|
||||
self._host,
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
errors = await self._connect_homee()
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
|
||||
data={
|
||||
CONF_HOST: self._host,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_HOST: self._name,
|
||||
},
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
@@ -108,12 +191,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
@@ -161,12 +244,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
errors["base"] = RESULT_CANNOT_CONNECT
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
errors["base"] = RESULT_INVALID_AUTH
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = RESULT_UNKNOWN_ERROR
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
|
@@ -20,6 +20,11 @@ from homeassistant.const import (
|
||||
# General
|
||||
DOMAIN = "homee"
|
||||
|
||||
# Error strings
|
||||
RESULT_CANNOT_CONNECT = "cannot_connect"
|
||||
RESULT_INVALID_AUTH = "invalid_auth"
|
||||
RESULT_UNKNOWN_ERROR = "unknown"
|
||||
|
||||
# Sensor mappings
|
||||
HOMEE_UNIT_TO_HA_UNIT = {
|
||||
"": None,
|
||||
|
@@ -8,5 +8,11 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyHomee==1.2.10"]
|
||||
"requirements": ["pyHomee==1.2.10"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_ssh._tcp.local.",
|
||||
"name": "homee-*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -46,6 +46,17 @@
|
||||
"data_description": {
|
||||
"host": "[%key:component::homee::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Configure discovered homee {host}",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::homee::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::homee::config::step::user::data_description::password%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -24,6 +24,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_HW_VERSION,
|
||||
ATTR_MODEL,
|
||||
ATTR_SW_VERSION,
|
||||
@@ -54,7 +55,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from .const import (
|
||||
ADMIN_SERVICES,
|
||||
ALL_KEYS,
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONF_MANUFACTURER,
|
||||
CONF_UNAUTHENTICATED_MODE,
|
||||
CONF_UPNP_UDN,
|
||||
|
@@ -2,8 +2,6 @@
|
||||
|
||||
DOMAIN = "huawei_lte"
|
||||
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
|
||||
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"
|
||||
|
@@ -8,12 +8,12 @@ from typing import Any
|
||||
from huawei_lte_api.exceptions import ResponseErrorException
|
||||
|
||||
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
||||
from homeassistant.const import CONF_RECIPIENT
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_RECIPIENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import Router
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -163,6 +163,7 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
@@ -197,6 +198,7 @@ async def async_setup_entry(
|
||||
name="group",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
|
@@ -53,6 +53,7 @@ class SensorManager:
|
||||
LOGGER,
|
||||
name="sensor",
|
||||
update_method=self.async_update_data,
|
||||
config_entry=bridge.config_entry,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
|
@@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CALENDAR,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.EVENT,
|
||||
Platform.LAWN_MOWER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
|
@@ -21,6 +21,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_reset_cutting_blade_usage_time(
|
||||
session: AutomowerSession,
|
||||
mower_id: str,
|
||||
) -> None:
|
||||
"""Reset cutting blade usage time."""
|
||||
await session.commands.reset_cutting_blade_usage_time(mower_id)
|
||||
|
||||
|
||||
def reset_cutting_blade_usage_time_availability(data: MowerAttributes) -> bool:
|
||||
"""Return True if blade usage time is greater than 0."""
|
||||
value = data.statistics.cutting_blade_usage_time
|
||||
return value is not None and value > 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Automower button entities."""
|
||||
@@ -28,6 +42,7 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
||||
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
|
||||
poll_after_sending: bool = False
|
||||
|
||||
|
||||
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
||||
@@ -43,6 +58,14 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
|
||||
translation_key="sync_clock",
|
||||
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
|
||||
),
|
||||
AutomowerButtonEntityDescription(
|
||||
key="reset_cutting_blade_usage_time",
|
||||
translation_key="reset_cutting_blade_usage_time",
|
||||
available_fn=reset_cutting_blade_usage_time_availability,
|
||||
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
|
||||
press_fn=async_reset_cutting_blade_usage_time,
|
||||
poll_after_sending=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -93,3 +116,5 @@ class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity):
|
||||
async def async_press(self) -> None:
|
||||
"""Send a command to the mower."""
|
||||
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
|
||||
if self.entity_description.poll_after_sending:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
@@ -17,3 +17,128 @@ ERROR_STATES = [
|
||||
MowerStates.WAIT_POWER_UP,
|
||||
MowerStates.WAIT_UPDATING,
|
||||
]
|
||||
|
||||
ERROR_KEYS = [
|
||||
"alarm_mower_in_motion",
|
||||
"alarm_mower_lifted",
|
||||
"alarm_mower_stopped",
|
||||
"alarm_mower_switched_off",
|
||||
"alarm_mower_tilted",
|
||||
"alarm_outside_geofence",
|
||||
"angular_sensor_problem",
|
||||
"battery_problem",
|
||||
"battery_restriction_due_to_ambient_temperature",
|
||||
"can_error",
|
||||
"charging_current_too_high",
|
||||
"charging_station_blocked",
|
||||
"charging_system_problem",
|
||||
"collision_sensor_defect",
|
||||
"collision_sensor_error",
|
||||
"collision_sensor_problem_front",
|
||||
"collision_sensor_problem_rear",
|
||||
"com_board_not_available",
|
||||
"communication_circuit_board_sw_must_be_updated",
|
||||
"complex_working_area",
|
||||
"connection_changed",
|
||||
"connection_not_changed",
|
||||
"connectivity_problem",
|
||||
"connectivity_settings_restored",
|
||||
"cutting_drive_motor_1_defect",
|
||||
"cutting_drive_motor_2_defect",
|
||||
"cutting_drive_motor_3_defect",
|
||||
"cutting_height_blocked",
|
||||
"cutting_height_problem",
|
||||
"cutting_height_problem_curr",
|
||||
"cutting_height_problem_dir",
|
||||
"cutting_height_problem_drive",
|
||||
"cutting_motor_problem",
|
||||
"cutting_stopped_slope_too_steep",
|
||||
"cutting_system_blocked",
|
||||
"cutting_system_imbalance_warning",
|
||||
"cutting_system_major_imbalance",
|
||||
"destination_not_reachable",
|
||||
"difficult_finding_home",
|
||||
"docking_sensor_defect",
|
||||
"electronic_problem",
|
||||
"empty_battery",
|
||||
"folding_cutting_deck_sensor_defect",
|
||||
"folding_sensor_activated",
|
||||
"geofence_problem",
|
||||
"gps_navigation_problem",
|
||||
"guide_1_not_found",
|
||||
"guide_2_not_found",
|
||||
"guide_3_not_found",
|
||||
"guide_calibration_accomplished",
|
||||
"guide_calibration_failed",
|
||||
"high_charging_power_loss",
|
||||
"high_internal_power_loss",
|
||||
"high_internal_temperature",
|
||||
"internal_voltage_error",
|
||||
"invalid_battery_combination_invalid_combination_of_different_battery_types",
|
||||
"invalid_sub_device_combination",
|
||||
"invalid_system_configuration",
|
||||
"left_brush_motor_overloaded",
|
||||
"lift_sensor_defect",
|
||||
"lifted",
|
||||
"limited_cutting_height_range",
|
||||
"loop_sensor_defect",
|
||||
"loop_sensor_problem_front",
|
||||
"loop_sensor_problem_left",
|
||||
"loop_sensor_problem_rear",
|
||||
"loop_sensor_problem_right",
|
||||
"low_battery",
|
||||
"memory_circuit_problem",
|
||||
"mower_lifted",
|
||||
"mower_tilted",
|
||||
"no_accurate_position_from_satellites",
|
||||
"no_confirmed_position",
|
||||
"no_drive",
|
||||
"no_loop_signal",
|
||||
"no_power_in_charging_station",
|
||||
"no_response_from_charger",
|
||||
"outside_working_area",
|
||||
"poor_signal_quality",
|
||||
"reference_station_communication_problem",
|
||||
"right_brush_motor_overloaded",
|
||||
"safety_function_faulty",
|
||||
"settings_restored",
|
||||
"sim_card_locked",
|
||||
"sim_card_not_found",
|
||||
"sim_card_requires_pin",
|
||||
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
||||
"slope_too_steep",
|
||||
"sms_could_not_be_sent",
|
||||
"stop_button_problem",
|
||||
"stuck_in_charging_station",
|
||||
"switch_cord_problem",
|
||||
"temporary_battery_problem",
|
||||
"tilt_sensor_problem",
|
||||
"too_high_discharge_current",
|
||||
"too_high_internal_current",
|
||||
"trapped",
|
||||
"ultrasonic_problem",
|
||||
"ultrasonic_sensor_1_defect",
|
||||
"ultrasonic_sensor_2_defect",
|
||||
"ultrasonic_sensor_3_defect",
|
||||
"ultrasonic_sensor_4_defect",
|
||||
"unexpected_cutting_height_adj",
|
||||
"unexpected_error",
|
||||
"upside_down",
|
||||
"weak_gps_signal",
|
||||
"wheel_drive_problem_left",
|
||||
"wheel_drive_problem_rear_left",
|
||||
"wheel_drive_problem_rear_right",
|
||||
"wheel_drive_problem_right",
|
||||
"wheel_motor_blocked_left",
|
||||
"wheel_motor_blocked_rear_left",
|
||||
"wheel_motor_blocked_rear_right",
|
||||
"wheel_motor_blocked_right",
|
||||
"wheel_motor_overloaded_left",
|
||||
"wheel_motor_overloaded_rear_left",
|
||||
"wheel_motor_overloaded_rear_right",
|
||||
"wheel_motor_overloaded_right",
|
||||
"work_area_not_valid",
|
||||
"wrong_loop_signal",
|
||||
"wrong_pin_code",
|
||||
"zone_generator_problem",
|
||||
]
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
@@ -12,9 +12,10 @@ from aioautomower.exceptions import (
|
||||
ApiError,
|
||||
AuthError,
|
||||
HusqvarnaTimeoutError,
|
||||
HusqvarnaWSClientError,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerDictionary
|
||||
from aioautomower.model import MowerDictionary, MowerStates
|
||||
from aioautomower.session import AutomowerSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -29,7 +30,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
MAX_WS_RECONNECT_TIME = 600
|
||||
SCAN_INTERVAL = timedelta(minutes=8)
|
||||
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
||||
|
||||
PONG_TIMEOUT = timedelta(seconds=90)
|
||||
PING_INTERVAL = timedelta(seconds=10)
|
||||
PING_TIMEOUT = timedelta(seconds=5)
|
||||
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
|
||||
|
||||
|
||||
@@ -58,6 +61,9 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
|
||||
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
|
||||
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
|
||||
self.pong: datetime | None = None
|
||||
self.websocket_alive: bool = False
|
||||
self._watchdog_task: asyncio.Task | None = None
|
||||
|
||||
@override
|
||||
@callback
|
||||
@@ -71,6 +77,18 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
await self.api.connect()
|
||||
self.api.register_data_callback(self.handle_websocket_updates)
|
||||
self.ws_connected = True
|
||||
|
||||
def start_watchdog() -> None:
|
||||
if self._watchdog_task is not None and not self._watchdog_task.done():
|
||||
_LOGGER.debug("Cancelling previous watchdog task")
|
||||
self._watchdog_task.cancel()
|
||||
self._watchdog_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._pong_watchdog(),
|
||||
"websocket_watchdog",
|
||||
)
|
||||
|
||||
self.api.register_ws_ready_callback(start_watchdog)
|
||||
try:
|
||||
data = await self.api.get_status()
|
||||
except ApiError as err:
|
||||
@@ -93,6 +111,19 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
mower_data.capabilities.work_areas for mower_data in self.data.values()
|
||||
):
|
||||
self._async_add_remove_work_areas()
|
||||
if (
|
||||
not self._should_poll()
|
||||
and self.update_interval is not None
|
||||
and self.websocket_alive
|
||||
):
|
||||
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
|
||||
self.update_interval = None
|
||||
if self.update_interval is None and self._should_poll():
|
||||
_LOGGER.debug(
|
||||
"Polling re-enabled via WebSocket: at least one mower active"
|
||||
)
|
||||
self.update_interval = SCAN_INTERVAL
|
||||
self.hass.async_create_task(self.async_request_refresh())
|
||||
|
||||
@callback
|
||||
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
|
||||
@@ -142,7 +173,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
# Reset reconnect time after successful connection
|
||||
self.reconnect_time = DEFAULT_RECONNECT_TIME
|
||||
await automower_client.start_listening()
|
||||
except HusqvarnaWSServerHandshakeError as err:
|
||||
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to websocket. Trying to reconnect: %s",
|
||||
err,
|
||||
@@ -161,6 +192,30 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
"reconnect_task",
|
||||
)
|
||||
|
||||
def _should_poll(self) -> bool:
|
||||
"""Return True if at least one mower is connected and at least one is not OFF."""
|
||||
return any(mower.metadata.connected for mower in self.data.values()) and any(
|
||||
mower.mower.state != MowerStates.OFF for mower in self.data.values()
|
||||
)
|
||||
|
||||
async def _pong_watchdog(self) -> None:
|
||||
_LOGGER.debug("Watchdog started")
|
||||
try:
|
||||
while True:
|
||||
_LOGGER.debug("Sending ping")
|
||||
self.websocket_alive = await self.api.send_empty_message()
|
||||
_LOGGER.debug("Ping result: %s", self.websocket_alive)
|
||||
|
||||
await asyncio.sleep(60)
|
||||
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
|
||||
if not self.websocket_alive:
|
||||
_LOGGER.debug("No pong received → restart polling")
|
||||
if self.update_interval is None:
|
||||
self.update_interval = SCAN_INTERVAL
|
||||
await self.async_request_refresh()
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Watchdog cancelled")
|
||||
|
||||
def _async_add_remove_devices(self) -> None:
|
||||
"""Add new devices and remove orphaned devices from the registry."""
|
||||
current_devices = set(self.data)
|
||||
|
108
homeassistant/components/husqvarna_automower/event.py
Normal file
108
homeassistant/components/husqvarna_automower/event.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Creates the event entities for supported mowers."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from aioautomower.model import SingleMessageData
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DOMAIN as EVENT_DOMAIN,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import ERROR_KEYS
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import AutomowerBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
ATTR_SEVERITY = "severity"
|
||||
ATTR_LATITUDE = "latitude"
|
||||
ATTR_LONGITUDE = "longitude"
|
||||
ATTR_DATE_TIME = "date_time"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AutomowerConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Automower message event entities.
|
||||
|
||||
Entities are created dynamically based on messages received from the API,
|
||||
but only for mowers that support message events.
|
||||
"""
|
||||
coordinator = config_entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
restored_mowers = {
|
||||
entry.unique_id.removesuffix("_message")
|
||||
for entry in er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
if entry.domain == EVENT_DOMAIN
|
||||
}
|
||||
|
||||
async_add_entities(
|
||||
AutomowerMessageEventEntity(mower_id, coordinator)
|
||||
for mower_id in restored_mowers
|
||||
if mower_id in coordinator.data
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_message(msg: SingleMessageData) -> None:
|
||||
if msg.id in restored_mowers:
|
||||
return
|
||||
|
||||
restored_mowers.add(msg.id)
|
||||
async_add_entities([AutomowerMessageEventEntity(msg.id, coordinator)])
|
||||
|
||||
coordinator.api.register_single_message_callback(_handle_message)
|
||||
|
||||
|
||||
class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
|
||||
"""EventEntity for Automower message events."""
|
||||
|
||||
entity_description: EventEntityDescription
|
||||
_message_cb: Callable[[SingleMessageData], None]
|
||||
_attr_translation_key = "message"
|
||||
_attr_event_types = ERROR_KEYS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mower_id: str,
|
||||
coordinator: AutomowerDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize Automower message event entity."""
|
||||
super().__init__(mower_id, coordinator)
|
||||
self._attr_unique_id = f"{mower_id}_message"
|
||||
|
||||
@callback
|
||||
def _handle(self, msg: SingleMessageData) -> None:
|
||||
"""Handle a message event from the API and trigger the event entity if it matches the entity's mower ID."""
|
||||
if msg.id != self.mower_id:
|
||||
return
|
||||
message = msg.attributes.message
|
||||
self._trigger_event(
|
||||
message.code,
|
||||
{
|
||||
ATTR_SEVERITY: message.severity,
|
||||
ATTR_LATITUDE: message.latitude,
|
||||
ATTR_LONGITUDE: message.longitude,
|
||||
ATTR_DATE_TIME: message.time,
|
||||
},
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.coordinator.api.register_single_message_callback(self._handle)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unregister WebSocket callback when entity is removed."""
|
||||
self.coordinator.api.unregister_single_message_callback(self._handle)
|
@@ -8,6 +8,14 @@
|
||||
"button": {
|
||||
"sync_clock": {
|
||||
"default": "mdi:clock-check-outline"
|
||||
},
|
||||
"reset_cutting_blade_usage_time": {
|
||||
"default": "mdi:saw-blade"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"message": {
|
||||
"default": "mdi:alert-circle-check-outline"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.1.1"]
|
||||
"requirements": ["aioautomower==2.1.2"]
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import ERROR_STATES
|
||||
from .const import ERROR_KEYS, ERROR_STATES
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import (
|
||||
AutomowerBaseEntity,
|
||||
@@ -42,135 +42,8 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
||||
|
||||
ERROR_KEYS = [
|
||||
"alarm_mower_in_motion",
|
||||
"alarm_mower_lifted",
|
||||
"alarm_mower_stopped",
|
||||
"alarm_mower_switched_off",
|
||||
"alarm_mower_tilted",
|
||||
"alarm_outside_geofence",
|
||||
"angular_sensor_problem",
|
||||
"battery_problem",
|
||||
"battery_restriction_due_to_ambient_temperature",
|
||||
"can_error",
|
||||
"charging_current_too_high",
|
||||
"charging_station_blocked",
|
||||
"charging_system_problem",
|
||||
"collision_sensor_defect",
|
||||
"collision_sensor_error",
|
||||
"collision_sensor_problem_front",
|
||||
"collision_sensor_problem_rear",
|
||||
"com_board_not_available",
|
||||
"communication_circuit_board_sw_must_be_updated",
|
||||
"complex_working_area",
|
||||
"connection_changed",
|
||||
"connection_not_changed",
|
||||
"connectivity_problem",
|
||||
"connectivity_settings_restored",
|
||||
"cutting_drive_motor_1_defect",
|
||||
"cutting_drive_motor_2_defect",
|
||||
"cutting_drive_motor_3_defect",
|
||||
"cutting_height_blocked",
|
||||
"cutting_height_problem_curr",
|
||||
"cutting_height_problem_dir",
|
||||
"cutting_height_problem_drive",
|
||||
"cutting_height_problem",
|
||||
"cutting_motor_problem",
|
||||
"cutting_stopped_slope_too_steep",
|
||||
"cutting_system_blocked",
|
||||
"cutting_system_imbalance_warning",
|
||||
"cutting_system_major_imbalance",
|
||||
"destination_not_reachable",
|
||||
"difficult_finding_home",
|
||||
"docking_sensor_defect",
|
||||
"electronic_problem",
|
||||
"empty_battery",
|
||||
"folding_cutting_deck_sensor_defect",
|
||||
"folding_sensor_activated",
|
||||
"geofence_problem",
|
||||
"gps_navigation_problem",
|
||||
"guide_1_not_found",
|
||||
"guide_2_not_found",
|
||||
"guide_3_not_found",
|
||||
"guide_calibration_accomplished",
|
||||
"guide_calibration_failed",
|
||||
"high_charging_power_loss",
|
||||
"high_internal_power_loss",
|
||||
"high_internal_temperature",
|
||||
"internal_voltage_error",
|
||||
"invalid_battery_combination_invalid_combination_of_different_battery_types",
|
||||
"invalid_sub_device_combination",
|
||||
"invalid_system_configuration",
|
||||
"left_brush_motor_overloaded",
|
||||
"lift_sensor_defect",
|
||||
"lifted",
|
||||
"limited_cutting_height_range",
|
||||
"loop_sensor_defect",
|
||||
"loop_sensor_problem_front",
|
||||
"loop_sensor_problem_left",
|
||||
"loop_sensor_problem_rear",
|
||||
"loop_sensor_problem_right",
|
||||
"low_battery",
|
||||
"memory_circuit_problem",
|
||||
"mower_lifted",
|
||||
"mower_tilted",
|
||||
"no_accurate_position_from_satellites",
|
||||
"no_confirmed_position",
|
||||
"no_drive",
|
||||
"no_error",
|
||||
"no_loop_signal",
|
||||
"no_power_in_charging_station",
|
||||
"no_response_from_charger",
|
||||
"outside_working_area",
|
||||
"poor_signal_quality",
|
||||
"reference_station_communication_problem",
|
||||
"right_brush_motor_overloaded",
|
||||
"safety_function_faulty",
|
||||
"settings_restored",
|
||||
"sim_card_locked",
|
||||
"sim_card_not_found",
|
||||
"sim_card_requires_pin",
|
||||
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
|
||||
"slope_too_steep",
|
||||
"sms_could_not_be_sent",
|
||||
"stop_button_problem",
|
||||
"stuck_in_charging_station",
|
||||
"switch_cord_problem",
|
||||
"temporary_battery_problem",
|
||||
"tilt_sensor_problem",
|
||||
"too_high_discharge_current",
|
||||
"too_high_internal_current",
|
||||
"trapped",
|
||||
"ultrasonic_problem",
|
||||
"ultrasonic_sensor_1_defect",
|
||||
"ultrasonic_sensor_2_defect",
|
||||
"ultrasonic_sensor_3_defect",
|
||||
"ultrasonic_sensor_4_defect",
|
||||
"unexpected_cutting_height_adj",
|
||||
"unexpected_error",
|
||||
"upside_down",
|
||||
"weak_gps_signal",
|
||||
"wheel_drive_problem_left",
|
||||
"wheel_drive_problem_rear_left",
|
||||
"wheel_drive_problem_rear_right",
|
||||
"wheel_drive_problem_right",
|
||||
"wheel_motor_blocked_left",
|
||||
"wheel_motor_blocked_rear_left",
|
||||
"wheel_motor_blocked_rear_right",
|
||||
"wheel_motor_blocked_right",
|
||||
"wheel_motor_overloaded_left",
|
||||
"wheel_motor_overloaded_rear_left",
|
||||
"wheel_motor_overloaded_rear_right",
|
||||
"wheel_motor_overloaded_right",
|
||||
"work_area_not_valid",
|
||||
"wrong_loop_signal",
|
||||
"wrong_pin_code",
|
||||
"zone_generator_problem",
|
||||
]
|
||||
|
||||
|
||||
ERROR_KEY_LIST = list(
|
||||
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||
ERROR_KEY_LIST = sorted(
|
||||
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
|
||||
)
|
||||
|
||||
INACTIVE_REASONS: list = [
|
||||
|
@@ -53,6 +53,161 @@
|
||||
},
|
||||
"sync_clock": {
|
||||
"name": "Sync clock"
|
||||
},
|
||||
"reset_cutting_blade_usage_time": {
|
||||
"name": "Reset cutting blade usage time"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"message": {
|
||||
"name": "Message",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"alarm_mower_in_motion": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_in_motion%]",
|
||||
"alarm_mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_lifted%]",
|
||||
"alarm_mower_stopped": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_stopped%]",
|
||||
"alarm_mower_switched_off": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_switched_off%]",
|
||||
"alarm_mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_tilted%]",
|
||||
"alarm_outside_geofence": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_outside_geofence%]",
|
||||
"angular_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::angular_sensor_problem%]",
|
||||
"battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_problem%]",
|
||||
"battery_restriction_due_to_ambient_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_restriction_due_to_ambient_temperature%]",
|
||||
"can_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::can_error%]",
|
||||
"charging_current_too_high": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_current_too_high%]",
|
||||
"charging_station_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_station_blocked%]",
|
||||
"charging_system_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_system_problem%]",
|
||||
"collision_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_defect%]",
|
||||
"collision_sensor_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_error%]",
|
||||
"collision_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_front%]",
|
||||
"collision_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_rear%]",
|
||||
"com_board_not_available": "[%key:component::husqvarna_automower::entity::sensor::error::state::com_board_not_available%]",
|
||||
"communication_circuit_board_sw_must_be_updated": "[%key:component::husqvarna_automower::entity::sensor::error::state::communication_circuit_board_sw_must_be_updated%]",
|
||||
"complex_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::complex_working_area%]",
|
||||
"connection_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_changed%]",
|
||||
"connection_not_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_not_changed%]",
|
||||
"connectivity_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_problem%]",
|
||||
"connectivity_settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_settings_restored%]",
|
||||
"cutting_drive_motor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_1_defect%]",
|
||||
"cutting_drive_motor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_2_defect%]",
|
||||
"cutting_drive_motor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_3_defect%]",
|
||||
"cutting_height_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_blocked%]",
|
||||
"cutting_height_problem_curr": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_curr%]",
|
||||
"cutting_height_problem_dir": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_dir%]",
|
||||
"cutting_height_problem_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_drive%]",
|
||||
"cutting_height_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem%]",
|
||||
"cutting_motor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_motor_problem%]",
|
||||
"cutting_stopped_slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_stopped_slope_too_steep%]",
|
||||
"cutting_system_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_blocked%]",
|
||||
"cutting_system_imbalance_warning": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_imbalance_warning%]",
|
||||
"cutting_system_major_imbalance": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_major_imbalance%]",
|
||||
"destination_not_reachable": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_not_reachable%]",
|
||||
"difficult_finding_home": "[%key:component::husqvarna_automower::entity::sensor::error::state::difficult_finding_home%]",
|
||||
"docking_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::docking_sensor_defect%]",
|
||||
"electronic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::electronic_problem%]",
|
||||
"empty_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::empty_battery%]",
|
||||
"error_at_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::error_at_power_up%]",
|
||||
"error": "[%key:common::state::error%]",
|
||||
"fatal_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::fatal_error%]",
|
||||
"folding_cutting_deck_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_cutting_deck_sensor_defect%]",
|
||||
"folding_sensor_activated": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_sensor_activated%]",
|
||||
"geofence_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::geofence_problem%]",
|
||||
"gps_navigation_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::gps_navigation_problem%]",
|
||||
"guide_1_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_1_not_found%]",
|
||||
"guide_2_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_2_not_found%]",
|
||||
"guide_3_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_3_not_found%]",
|
||||
"guide_calibration_accomplished": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_accomplished%]",
|
||||
"guide_calibration_failed": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_failed%]",
|
||||
"high_charging_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_charging_power_loss%]",
|
||||
"high_internal_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_power_loss%]",
|
||||
"high_internal_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_temperature%]",
|
||||
"internal_voltage_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::internal_voltage_error%]",
|
||||
"invalid_battery_combination_invalid_combination_of_different_battery_types": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_battery_combination_invalid_combination_of_different_battery_types%]",
|
||||
"invalid_sub_device_combination": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_sub_device_combination%]",
|
||||
"invalid_system_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_system_configuration%]",
|
||||
"left_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::left_brush_motor_overloaded%]",
|
||||
"lift_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::lift_sensor_defect%]",
|
||||
"lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted%]",
|
||||
"limited_cutting_height_range": "[%key:component::husqvarna_automower::entity::sensor::error::state::limited_cutting_height_range%]",
|
||||
"loop_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_defect%]",
|
||||
"loop_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_front%]",
|
||||
"loop_sensor_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_left%]",
|
||||
"loop_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_rear%]",
|
||||
"loop_sensor_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_right%]",
|
||||
"low_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::low_battery%]",
|
||||
"memory_circuit_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::memory_circuit_problem%]",
|
||||
"mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_lifted%]",
|
||||
"mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_tilted%]",
|
||||
"no_accurate_position_from_satellites": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_accurate_position_from_satellites%]",
|
||||
"no_confirmed_position": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_confirmed_position%]",
|
||||
"no_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_drive%]",
|
||||
"no_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_error%]",
|
||||
"no_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_loop_signal%]",
|
||||
"no_power_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_power_in_charging_station%]",
|
||||
"no_response_from_charger": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_response_from_charger%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"outside_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::outside_working_area%]",
|
||||
"poor_signal_quality": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_signal_quality%]",
|
||||
"reference_station_communication_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::reference_station_communication_problem%]",
|
||||
"right_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::right_brush_motor_overloaded%]",
|
||||
"safety_function_faulty": "[%key:component::husqvarna_automower::entity::sensor::error::state::safety_function_faulty%]",
|
||||
"settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::settings_restored%]",
|
||||
"sim_card_locked": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_locked%]",
|
||||
"sim_card_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_not_found%]",
|
||||
"sim_card_requires_pin": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_requires_pin%]",
|
||||
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "[%key:component::husqvarna_automower::entity::sensor::error::state::slipped_mower_has_slipped_situation_not_solved_with_moving_pattern%]",
|
||||
"slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::slope_too_steep%]",
|
||||
"sms_could_not_be_sent": "[%key:component::husqvarna_automower::entity::sensor::error::state::sms_could_not_be_sent%]",
|
||||
"stop_button_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::stop_button_problem%]",
|
||||
"stopped": "[%key:common::state::stopped%]",
|
||||
"stuck_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::stuck_in_charging_station%]",
|
||||
"switch_cord_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::switch_cord_problem%]",
|
||||
"temporary_battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::temporary_battery_problem%]",
|
||||
"tilt_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::tilt_sensor_problem%]",
|
||||
"too_high_discharge_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_discharge_current%]",
|
||||
"too_high_internal_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_internal_current%]",
|
||||
"trapped": "[%key:component::husqvarna_automower::entity::sensor::error::state::trapped%]",
|
||||
"ultrasonic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_problem%]",
|
||||
"ultrasonic_sensor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_1_defect%]",
|
||||
"ultrasonic_sensor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_2_defect%]",
|
||||
"ultrasonic_sensor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_3_defect%]",
|
||||
"ultrasonic_sensor_4_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_4_defect%]",
|
||||
"unexpected_cutting_height_adj": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_cutting_height_adj%]",
|
||||
"unexpected_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_error%]",
|
||||
"upside_down": "[%key:component::husqvarna_automower::entity::sensor::error::state::upside_down%]",
|
||||
"wait_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_power_up%]",
|
||||
"wait_updating": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_updating%]",
|
||||
"weak_gps_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::weak_gps_signal%]",
|
||||
"wheel_drive_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_left%]",
|
||||
"wheel_drive_problem_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_left%]",
|
||||
"wheel_drive_problem_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_right%]",
|
||||
"wheel_drive_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_right%]",
|
||||
"wheel_motor_blocked_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_left%]",
|
||||
"wheel_motor_blocked_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_left%]",
|
||||
"wheel_motor_blocked_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_right%]",
|
||||
"wheel_motor_blocked_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_right%]",
|
||||
"wheel_motor_overloaded_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_left%]",
|
||||
"wheel_motor_overloaded_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_left%]",
|
||||
"wheel_motor_overloaded_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_right%]",
|
||||
"wheel_motor_overloaded_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_right%]",
|
||||
"work_area_not_valid": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_not_valid%]",
|
||||
"wrong_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_loop_signal%]",
|
||||
"wrong_pin_code": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_pin_code%]",
|
||||
"zone_generator_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::zone_generator_problem%]"
|
||||
}
|
||||
},
|
||||
"severity": {
|
||||
"state": {
|
||||
"fatal": "Fatal",
|
||||
"error": "[%key:common::state::error%]",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"debug": "Debug",
|
||||
"sw": "Software",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.1"]
|
||||
"requirements": ["automower-ble==0.2.7"]
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huum",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["huum==0.8.0"]
|
||||
"requirements": ["huum==0.8.1"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.5.1"]
|
||||
"requirements": ["imgw_pib==1.5.3"]
|
||||
}
|
||||
|
@@ -42,10 +42,19 @@
|
||||
"local_name": "Ink@IAM-T1",
|
||||
"connectable": true
|
||||
},
|
||||
{
|
||||
"local_name": "Ink@IAM-T2",
|
||||
"connectable": true
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 12628,
|
||||
"manufacturer_data_start": [65, 67, 45],
|
||||
"connectable": true
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 12884,
|
||||
"manufacturer_data_start": [0, 98, 0],
|
||||
"connectable": false
|
||||
}
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
@@ -53,5 +62,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.16.2"]
|
||||
"requirements": ["inkbird-ble==1.1.0"]
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user