mirror of
https://github.com/home-assistant/core.git
synced 2025-12-26 09:49:21 +00:00
Compare commits
472 Commits
mill_devic
...
climate-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad5ce6d52 | ||
|
|
a117a3cba9 | ||
|
|
e2d9ca9cd9 | ||
|
|
a0fb6df5ba | ||
|
|
04020d5a56 | ||
|
|
f785b17314 | ||
|
|
6631c57cfb | ||
|
|
bc76dc3c34 | ||
|
|
ea4931ca3a | ||
|
|
dd20204bf0 | ||
|
|
ef46c62bc6 | ||
|
|
2bb6e03a36 | ||
|
|
2288f89415 | ||
|
|
e7ab5afc14 | ||
|
|
4db88dfaff | ||
|
|
906c95048c | ||
|
|
df38c1b1d7 | ||
|
|
af97bf1c5f | ||
|
|
a7c2d96ecf | ||
|
|
1b06b4e45b | ||
|
|
b74b9bc360 | ||
|
|
810689ce66 | ||
|
|
249d93574a | ||
|
|
e2c59f276a | ||
|
|
9804e8aa98 | ||
|
|
53e69af088 | ||
|
|
1530edbe20 | ||
|
|
7dbf32d693 | ||
|
|
49646ad994 | ||
|
|
1e652db37f | ||
|
|
88d366b0c5 | ||
|
|
65147f8d4c | ||
|
|
52b919101a | ||
|
|
24fd74d839 | ||
|
|
2599faa622 | ||
|
|
3df91cfba5 | ||
|
|
d3fab42c85 | ||
|
|
beb881492a | ||
|
|
9d7c7f9fcf | ||
|
|
419307a7c4 | ||
|
|
409dc4ad48 | ||
|
|
7704ef95a4 | ||
|
|
0db07a033b | ||
|
|
4717eb3142 | ||
|
|
c23f5c9f2c | ||
|
|
873b078bb3 | ||
|
|
0dd93a18c5 | ||
|
|
da96e2077b | ||
|
|
1d69cf11a5 | ||
|
|
adb1fbbbc4 | ||
|
|
645f2e44b9 | ||
|
|
b3aede611a | ||
|
|
72a96249b1 | ||
|
|
80dbce14ec | ||
|
|
0376f75ee3 | ||
|
|
e58bd62c68 | ||
|
|
6dbcd130b0 | ||
|
|
4639f57014 | ||
|
|
4080455c12 | ||
|
|
df7d518f38 | ||
|
|
47adfb574f | ||
|
|
4c5d0c2ec4 | ||
|
|
4febe43021 | ||
|
|
af13979855 | ||
|
|
d9f2140df3 | ||
|
|
cc80108629 | ||
|
|
16af76b968 | ||
|
|
590f0ce61f | ||
|
|
14059c6df8 | ||
|
|
268c21addd | ||
|
|
565fa4ea1f | ||
|
|
28cd7f2473 | ||
|
|
aceb1b39ba | ||
|
|
6edf06f8a4 | ||
|
|
07ae9b15d0 | ||
|
|
d676169b04 | ||
|
|
24ce3d7daa | ||
|
|
417e736746 | ||
|
|
bb8d4ca255 | ||
|
|
375af6cb1c | ||
|
|
263e0acd3a | ||
|
|
da531d0e4e | ||
|
|
844e36c8fe | ||
|
|
9976c07f89 | ||
|
|
7df9d2e938 | ||
|
|
52318f5f37 | ||
|
|
b9c2b3f7e3 | ||
|
|
a9ff5b8007 | ||
|
|
7076ba7c9d | ||
|
|
5e0088feaa | ||
|
|
f8399b2c0f | ||
|
|
415fdf4956 | ||
|
|
ad89004189 | ||
|
|
b6afbe4b29 | ||
|
|
402340955e | ||
|
|
b2a160d926 | ||
|
|
9840785363 | ||
|
|
a53c92d4b5 | ||
|
|
adc97b6c15 | ||
|
|
7b2a5d0684 | ||
|
|
acb511d395 | ||
|
|
c025390c6c | ||
|
|
942fbdedcf | ||
|
|
3bfb6707e9 | ||
|
|
5172139579 | ||
|
|
cfb43c7b58 | ||
|
|
45657ece7c | ||
|
|
f7fe2f2122 | ||
|
|
c75222e63c | ||
|
|
299250ebec | ||
|
|
ed8e242049 | ||
|
|
95e4a40ad5 | ||
|
|
e61717ce7a | ||
|
|
73b6bd8bd3 | ||
|
|
60774c69cd | ||
|
|
c383b41a12 | ||
|
|
05a8b773b9 | ||
|
|
1bee423c22 | ||
|
|
687afd23bc | ||
|
|
0020c48a15 | ||
|
|
760cbcc596 | ||
|
|
da8f4e5b57 | ||
|
|
5c0659c8df | ||
|
|
15806c2af6 | ||
|
|
97d8d16cc5 | ||
|
|
33435fa36f | ||
|
|
6fc1cfded9 | ||
|
|
a9d6a42781 | ||
|
|
f2a706ecf7 | ||
|
|
4a2ae7f6fd | ||
|
|
771ead9d7b | ||
|
|
2d5e2aa4b4 | ||
|
|
6f11524b84 | ||
|
|
561f319e3b | ||
|
|
0c9ec4b699 | ||
|
|
cbb2930805 | ||
|
|
aa29a93fbe | ||
|
|
ff4ba553c4 | ||
|
|
2f101c5054 | ||
|
|
72e2b835d9 | ||
|
|
8f6e4cd294 | ||
|
|
bd0edd4996 | ||
|
|
3f441e7090 | ||
|
|
253098d79c | ||
|
|
53ebf84339 | ||
|
|
7cfbc3eeae | ||
|
|
8d32531bc1 | ||
|
|
30d95f37d8 | ||
|
|
bbb5f9e717 | ||
|
|
6cbc803b28 | ||
|
|
abe00884ea | ||
|
|
0cbc77ad3f | ||
|
|
5487e8673c | ||
|
|
45ae2f4736 | ||
|
|
8e86c3c775 | ||
|
|
5ef12c3993 | ||
|
|
43a420cf01 | ||
|
|
70648da8fd | ||
|
|
7f6a77ad2f | ||
|
|
386a722393 | ||
|
|
c5fe25a001 | ||
|
|
cf3d4eb26a | ||
|
|
939365887f | ||
|
|
e3cf5c47b2 | ||
|
|
b2170ad732 | ||
|
|
ed7da35de4 | ||
|
|
83f5ca5a30 | ||
|
|
1f8f85d6eb | ||
|
|
59d8c79371 | ||
|
|
a6f6317299 | ||
|
|
b1fe247eed | ||
|
|
8991cd4f46 | ||
|
|
9e1ba004d4 | ||
|
|
ddb3edca5d | ||
|
|
4321d27ed3 | ||
|
|
ad0ee8f2d6 | ||
|
|
6cdbdadc24 | ||
|
|
cf45c67055 | ||
|
|
3658cdba4c | ||
|
|
dcc9be02ca | ||
|
|
de1b6a0dfc | ||
|
|
29fa40a5cf | ||
|
|
67f0de441b | ||
|
|
353f085474 | ||
|
|
00a1ae0eeb | ||
|
|
df26166047 | ||
|
|
8ab936b87c | ||
|
|
4ed0c21a4a | ||
|
|
a3657a0fef | ||
|
|
74b425a06e | ||
|
|
0560b634e3 | ||
|
|
8eebbd45bd | ||
|
|
c9ad87d464 | ||
|
|
c2358d5158 | ||
|
|
2d2b979c7d | ||
|
|
ebcb478f52 | ||
|
|
0f18f128fd | ||
|
|
368e958457 | ||
|
|
26180486e7 | ||
|
|
0ad9af0feb | ||
|
|
07322c6992 | ||
|
|
b1f6563fb2 | ||
|
|
475f19c140 | ||
|
|
344a03d9ce | ||
|
|
6c70586f7e | ||
|
|
0f1835139f | ||
|
|
de5a49363e | ||
|
|
c6789d70a4 | ||
|
|
a2aba77973 | ||
|
|
84a3a9d495 | ||
|
|
d994884726 | ||
|
|
0e9965150e | ||
|
|
feca7c28cf | ||
|
|
0ba32e1d3a | ||
|
|
6179da4321 | ||
|
|
93c0eb73d2 | ||
|
|
484f149e61 | ||
|
|
3cc75c3cf6 | ||
|
|
26d5c55d11 | ||
|
|
959f20c523 | ||
|
|
075f95b9c4 | ||
|
|
1e68ae1bb8 | ||
|
|
5ef3901b44 | ||
|
|
56b58cec3e | ||
|
|
cdd73a5c5a | ||
|
|
88eb550ec1 | ||
|
|
7be3cad1db | ||
|
|
31c6443a9b | ||
|
|
84d359c0d9 | ||
|
|
619aed39b7 | ||
|
|
3f1acff652 | ||
|
|
c3d0a01776 | ||
|
|
cd6da9d9e8 | ||
|
|
0c24afec6c | ||
|
|
284ccbc778 | ||
|
|
cef182c596 | ||
|
|
d322398d06 | ||
|
|
c2a9b0ff52 | ||
|
|
662dea28ed | ||
|
|
9fcf8f22d2 | ||
|
|
9dc20b5709 | ||
|
|
5e4e1ce5a7 | ||
|
|
85519a312c | ||
|
|
81ce5f4505 | ||
|
|
c67e2047e3 | ||
|
|
04276d3523 | ||
|
|
f2df57e230 | ||
|
|
0037799bfe | ||
|
|
944ad9022d | ||
|
|
4b6febc757 | ||
|
|
ac2090d2f5 | ||
|
|
3dad5f6896 | ||
|
|
cc134c820b | ||
|
|
ef31413a59 | ||
|
|
9292bfc6ed | ||
|
|
9e6c1d5b62 | ||
|
|
7e2d382ff4 | ||
|
|
b5a7a41ebe | ||
|
|
a3febc4449 | ||
|
|
5abc03c21e | ||
|
|
dc9133f919 | ||
|
|
a3fad89d0d | ||
|
|
a3fab094c3 | ||
|
|
11efec49db | ||
|
|
6b666b3a0f | ||
|
|
7e24b353ac | ||
|
|
5665abf991 | ||
|
|
6314d7a44c | ||
|
|
b106b88f5c | ||
|
|
dbe04f17ad | ||
|
|
aad1d6a25d | ||
|
|
7326555f03 | ||
|
|
5c2d769b54 | ||
|
|
66e863a2e3 | ||
|
|
78c9e47428 | ||
|
|
4e316429d3 | ||
|
|
989a3d1e24 | ||
|
|
4efcf18c70 | ||
|
|
7998a05742 | ||
|
|
4ee9f813aa | ||
|
|
859993e443 | ||
|
|
4a063c3f9e | ||
|
|
43fab48d4e | ||
|
|
02785a4ded | ||
|
|
e43f4466e0 | ||
|
|
82f54eb9d2 | ||
|
|
954b6133cb | ||
|
|
9c70ec4150 | ||
|
|
3788e942a7 | ||
|
|
e38a85da64 | ||
|
|
861d9b3341 | ||
|
|
b6819cbff3 | ||
|
|
9a0035e090 | ||
|
|
1e420f16f7 | ||
|
|
b29be34f55 | ||
|
|
8607ba884c | ||
|
|
2639bdbefd | ||
|
|
c780933fa0 | ||
|
|
a23b37114e | ||
|
|
17f0c24895 | ||
|
|
6ed345f773 | ||
|
|
233395c181 | ||
|
|
92195ff77d | ||
|
|
ad7a334147 | ||
|
|
87f5a7057e | ||
|
|
5afb9a5053 | ||
|
|
1c0135880d | ||
|
|
f49111a4d9 | ||
|
|
10478f4ca5 | ||
|
|
9f43a7a17b | ||
|
|
cbb4c06195 | ||
|
|
83fdc07df0 | ||
|
|
0d309aa632 | ||
|
|
7d04eef5c5 | ||
|
|
870dc4dbea | ||
|
|
4c96b83297 | ||
|
|
5834ecb13e | ||
|
|
e62a563ec1 | ||
|
|
6a599dc27a | ||
|
|
df383a3a31 | ||
|
|
86e43b7196 | ||
|
|
b5c4608373 | ||
|
|
bddd8624bb | ||
|
|
6974f61703 | ||
|
|
3df992790d | ||
|
|
b391dfe647 | ||
|
|
e7bdf1467b | ||
|
|
ff9df15cb0 | ||
|
|
fdde9d3a52 | ||
|
|
8c1a18b383 | ||
|
|
367749d93c | ||
|
|
13f32c6720 | ||
|
|
f8e1a786be | ||
|
|
cd5a46f11d | ||
|
|
0140aa7240 | ||
|
|
92f50c63b1 | ||
|
|
1afeabfd64 | ||
|
|
709d15a79b | ||
|
|
cf4dbcfebf | ||
|
|
59e6fa5138 | ||
|
|
8400ef8441 | ||
|
|
6188db18c2 | ||
|
|
7e6392f062 | ||
|
|
10191e7a23 | ||
|
|
ad34bc8910 | ||
|
|
26212798a3 | ||
|
|
3d20c5c5d6 | ||
|
|
afae257a12 | ||
|
|
64aba0c1a3 | ||
|
|
551a584ca6 | ||
|
|
b261c7f18a | ||
|
|
61e5f10d12 | ||
|
|
2413fc4c0d | ||
|
|
e6ef3fe507 | ||
|
|
04bcc8d3d3 | ||
|
|
52683c5f75 | ||
|
|
2f77cda822 | ||
|
|
a97434976e | ||
|
|
e357e0a406 | ||
|
|
1a068d99d6 | ||
|
|
95b3d27b60 | ||
|
|
a3ef3cce3e | ||
|
|
255f85eb2f | ||
|
|
94c7d18346 | ||
|
|
eb8ee1339c | ||
|
|
962f1bad32 | ||
|
|
dd215b3d5d | ||
|
|
bb7abd037c | ||
|
|
d35b34f142 | ||
|
|
1c119518db | ||
|
|
9a6c749714 | ||
|
|
79484ea7f5 | ||
|
|
3568bdca65 | ||
|
|
a76f82080b | ||
|
|
cd384cadbe | ||
|
|
69a8d3f3c1 | ||
|
|
a3fb6e8f92 | ||
|
|
c8480627ca | ||
|
|
893f605d61 | ||
|
|
ddd2ba6c4a | ||
|
|
681863f80e | ||
|
|
99698ef95d | ||
|
|
3fe08a7223 | ||
|
|
35601480d2 | ||
|
|
0076bd8389 | ||
|
|
9f3c549f8d | ||
|
|
03707e6308 | ||
|
|
9e6a8638dd | ||
|
|
2a9082559a | ||
|
|
ba3fca53b0 | ||
|
|
e4bb351d2d | ||
|
|
1bdda0249e | ||
|
|
ff8bc763c3 | ||
|
|
8a8be71f96 | ||
|
|
19e6867f1a | ||
|
|
c8f050ecbc | ||
|
|
b7ff27122a | ||
|
|
3a8b0b3ea6 | ||
|
|
0ff2a0d66d | ||
|
|
4daf6dd41d | ||
|
|
51bead3229 | ||
|
|
352e948d56 | ||
|
|
70ad4ee454 | ||
|
|
53ef96c63e | ||
|
|
bb2d027532 | ||
|
|
51d63ba508 | ||
|
|
fc622e398f | ||
|
|
920de90603 | ||
|
|
a6089b497a | ||
|
|
5516f3609d | ||
|
|
a1558213c4 | ||
|
|
2564533dae | ||
|
|
f46e764982 | ||
|
|
d6c201de4a | ||
|
|
c9f1829c0b | ||
|
|
1e075cdac7 | ||
|
|
fce6d6246f | ||
|
|
3132700492 | ||
|
|
943b1d9f08 | ||
|
|
2d6d313e5c | ||
|
|
9716183997 | ||
|
|
a46a0ad2b4 | ||
|
|
c06bc53724 | ||
|
|
4399d09820 | ||
|
|
ca2c7280eb | ||
|
|
ecb3bf79f3 | ||
|
|
2aba1d399b | ||
|
|
be25cb7aa7 | ||
|
|
3bb6256572 | ||
|
|
fc4100833e | ||
|
|
992afc4cd3 | ||
|
|
7730f423b3 | ||
|
|
05b0c56191 | ||
|
|
fa0e54e658 | ||
|
|
869a0d7abc | ||
|
|
90208d2eb1 | ||
|
|
a6520d2627 | ||
|
|
8b8c409916 | ||
|
|
a2be5a383c | ||
|
|
39d781905d | ||
|
|
5fb5e933e2 | ||
|
|
413a578fdb | ||
|
|
c1cf0e23b2 | ||
|
|
a449ca65be | ||
|
|
4c91d1b402 | ||
|
|
fab92d1cf8 | ||
|
|
c10473844f | ||
|
|
dfdd83789a | ||
|
|
9bff9c5e7b | ||
|
|
e73512e11c | ||
|
|
4c60e36f4f | ||
|
|
f8cd6204ca | ||
|
|
eae25023e7 | ||
|
|
21c3bf48f9 | ||
|
|
a5eb816dcf | ||
|
|
517f3faa0a | ||
|
|
b4015805f7 | ||
|
|
a56ad0273b | ||
|
|
2bc917c842 | ||
|
|
97f22b3a3d | ||
|
|
a48a5adc81 | ||
|
|
eb86b00dd4 | ||
|
|
e93256951e | ||
|
|
3b0ab421b0 | ||
|
|
ca47253d81 | ||
|
|
9b0a489753 | ||
|
|
9b02db008e | ||
|
|
223817a7fb | ||
|
|
cdea9b5d3a | ||
|
|
8286ec9e60 | ||
|
|
cce7b9ac34 | ||
|
|
a42c0230c9 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.1"
|
||||
HA_SHORT_VERSION: "2025.2"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -537,7 +537,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@@ -661,7 +661,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
@@ -877,7 +877,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -979,14 +979,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1106,7 +1106,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1114,7 +1114,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1236,7 +1236,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1244,7 +1244,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1273,7 +1273,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.1.1
|
||||
uses: codecov/codecov-action@v5.1.2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1378,14 +1378,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1411,7 +1411,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.1.1
|
||||
uses: codecov/codecov-action@v5.1.2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
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.27.9
|
||||
uses: github/codeql-action/init@v3.28.0
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.27.9
|
||||
uses: github/codeql-action/analyze@v3.28.0
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
35
.github/workflows/wheels.yml
vendored
35
.github/workflows/wheels.yml
vendored
@@ -76,18 +76,37 @@ jobs:
|
||||
|
||||
# Use C-Extension for SQLAlchemy
|
||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||
|
||||
# Add additional pip wheel build constraints
|
||||
echo "PIP_CONSTRAINT=build_constraints.txt"
|
||||
) > .env_file
|
||||
|
||||
- name: Write pip wheel build constraints
|
||||
run: |
|
||||
(
|
||||
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
|
||||
# this caused the numpy builds to fail
|
||||
# https://github.com/scikit-build/ninja-python-distributions/issues/274
|
||||
echo "ninja==1.11.1.1"
|
||||
) > build_constraints.txt
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
|
||||
- name: Upload build_constraints
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: build_constraints
|
||||
path: ./build_constraints.txt
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -99,7 +118,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -123,6 +142,11 @@ jobs:
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
@@ -167,6 +191,11 @@ jobs:
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
|
||||
@@ -12,7 +12,7 @@ repos:
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||
- --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
|
||||
@@ -311,6 +311,7 @@ homeassistant.components.manual.*
|
||||
homeassistant.components.mastodon.*
|
||||
homeassistant.components.matrix.*
|
||||
homeassistant.components.matter.*
|
||||
homeassistant.components.mealie.*
|
||||
homeassistant.components.media_extractor.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
@@ -361,8 +362,10 @@ homeassistant.components.openuv.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
homeassistant.components.overseerr.*
|
||||
homeassistant.components.p1_monitor.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.peblar.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.pi_hole.*
|
||||
|
||||
29
CODEOWNERS
29
CODEOWNERS
@@ -578,8 +578,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_tasks/ @allenporter
|
||||
/homeassistant/components/google_travel_time/ @eifinger
|
||||
/tests/components/google_travel_time/ @eifinger
|
||||
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/tests/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/homeassistant/components/govee_ble/ @bdraco
|
||||
/tests/components/govee_ble/ @bdraco
|
||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||
/tests/components/govee_light_local/ @Galorhallen
|
||||
/homeassistant/components/gpsd/ @fabaff @jrieger
|
||||
@@ -1066,8 +1066,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||
/tests/components/onewire/ @garbled1 @epenet
|
||||
/homeassistant/components/onkyo/ @arturpragacz
|
||||
/tests/components/onkyo/ @arturpragacz
|
||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||
/homeassistant/components/onvif/ @hunterjm
|
||||
/tests/components/onvif/ @hunterjm
|
||||
/homeassistant/components/open_meteo/ @frenck
|
||||
@@ -1103,8 +1103,10 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ourgroceries/ @OnFreund
|
||||
/tests/components/ourgroceries/ @OnFreund
|
||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/homeassistant/components/overkiz/ @imicknl
|
||||
/tests/components/overkiz/ @imicknl
|
||||
/homeassistant/components/overseerr/ @joostlek
|
||||
/tests/components/overseerr/ @joostlek
|
||||
/homeassistant/components/ovo_energy/ @timmo001
|
||||
/tests/components/ovo_energy/ @timmo001
|
||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||
@@ -1113,6 +1115,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/palazzetti/ @dotvav
|
||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||
/tests/components/panel_custom/ @home-assistant/frontend
|
||||
/homeassistant/components/peblar/ @frenck
|
||||
/tests/components/peblar/ @frenck
|
||||
/homeassistant/components/peco/ @IceBotYT
|
||||
/tests/components/peco/ @IceBotYT
|
||||
/homeassistant/components/pegel_online/ @mib1185
|
||||
@@ -1133,8 +1137,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/plaato/ @JohNan
|
||||
/homeassistant/components/plex/ @jjlawren
|
||||
/tests/components/plex/ @jjlawren
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck
|
||||
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||
/tests/components/plugwise/ @CoMPaTech @bouwew
|
||||
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||
/homeassistant/components/point/ @fredrike
|
||||
@@ -1476,8 +1480,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/tado/ @chiefdragon @erwindouna
|
||||
/tests/components/tado/ @chiefdragon @erwindouna
|
||||
/homeassistant/components/tado/ @erwindouna
|
||||
/tests/components/tado/ @erwindouna
|
||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||
/tests/components/tag/ @balloob @dmulcahey
|
||||
/homeassistant/components/tailscale/ @frenck
|
||||
@@ -1571,8 +1575,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/triggercmd/ @rvmey
|
||||
/homeassistant/components/tts/ @home-assistant/core
|
||||
/tests/components/tts/ @home-assistant/core
|
||||
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
|
||||
/tests/components/tuya/ @Tuya @zlinoliver @frenck
|
||||
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
||||
/tests/components/tuya/ @Tuya @zlinoliver
|
||||
/homeassistant/components/twentemilieu/ @frenck
|
||||
/tests/components/twentemilieu/ @frenck
|
||||
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
||||
@@ -1740,6 +1744,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/youtube/ @joostlek
|
||||
/tests/components/youtube/ @joostlek
|
||||
/homeassistant/components/zabbix/ @kruton
|
||||
/homeassistant/components/zamg/ @killer0071234
|
||||
/tests/components/zamg/ @killer0071234
|
||||
/homeassistant/components/zengge/ @emontnemery
|
||||
|
||||
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.1
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -64,6 +64,9 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
||||
)
|
||||
except (FileNotFoundError, KeyError, json.JSONDecodeError):
|
||||
return None
|
||||
finally:
|
||||
# Always remove the backup instruction file to prevent a boot loop
|
||||
instruction_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _clear_configuration_directory(config_dir: Path, keep: Iterable[str]) -> None:
|
||||
|
||||
@@ -252,6 +252,7 @@ PRELOAD_STORAGE = [
|
||||
"assist_pipeline.pipelines",
|
||||
"core.analytics",
|
||||
"auth_module.totp",
|
||||
"backup",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "microsoft",
|
||||
"name": "Microsoft",
|
||||
"integrations": [
|
||||
"azure_data_explorer",
|
||||
"azure_devops",
|
||||
"azure_event_hub",
|
||||
"azure_service_bus",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
from aemet_opendata.interface import AEMET, ConnectionOptions, UpdateFeature
|
||||
@@ -10,8 +11,9 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DOMAIN, PLATFORMS
|
||||
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,11 +26,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
update_features: int = UpdateFeature.FORECAST
|
||||
if entry.options.get(CONF_RADAR_UPDATES, False):
|
||||
update_features |= UpdateFeature.RADAR
|
||||
if entry.options.get(CONF_STATION_UPDATES, True):
|
||||
update_features |= UpdateFeature.STATION
|
||||
|
||||
options = ConnectionOptions(api_key, update_features)
|
||||
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
|
||||
aemet.set_api_data_dir(hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"))
|
||||
|
||||
try:
|
||||
await aemet.select_coordinates(latitude, longitude)
|
||||
except TownNotFound as err:
|
||||
@@ -57,3 +63,11 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
await hass.async_add_executor_job(
|
||||
shutil.rmtree,
|
||||
hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"),
|
||||
)
|
||||
|
||||
@@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
|
||||
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_RADAR_UPDATES, default=False): bool,
|
||||
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -51,8 +51,9 @@ from homeassistant.components.weather import (
|
||||
from homeassistant.const import Platform
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
CONF_RADAR_UPDATES = "radar_updates"
|
||||
CONF_STATION_UPDATES = "station_updates"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aemet_opendata.const import AOD_COORDS
|
||||
from aemet_opendata.const import AOD_COORDS, AOD_IMG_BYTES
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import (
|
||||
@@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
|
||||
|
||||
TO_REDACT_COORD = [
|
||||
AOD_COORDS,
|
||||
AOD_IMG_BYTES,
|
||||
]
|
||||
|
||||
|
||||
|
||||
86
homeassistant/components/aemet/image.py
Normal file
86
homeassistant/components/aemet/image.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Support for the AEMET OpenData images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from aemet_opendata.const import AOD_DATETIME, AOD_IMG_BYTES, AOD_IMG_TYPE, AOD_RADAR
|
||||
from aemet_opendata.helpers import dict_nested_value
|
||||
|
||||
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
AEMET_IMAGES: Final[tuple[ImageEntityDescription, ...]] = (
|
||||
ImageEntityDescription(
|
||||
key=AOD_RADAR,
|
||||
translation_key="weather_radar",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData image entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
coordinator = domain_data.coordinator
|
||||
|
||||
unique_id = config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
|
||||
async_add_entities(
|
||||
AemetImage(
|
||||
hass,
|
||||
name,
|
||||
coordinator,
|
||||
description,
|
||||
unique_id,
|
||||
)
|
||||
for description in AEMET_IMAGES
|
||||
if dict_nested_value(coordinator.data["lib"], [description.key]) is not None
|
||||
)
|
||||
|
||||
|
||||
class AemetImage(AemetEntity, ImageEntity):
|
||||
"""Implementation of an AEMET OpenData image."""
|
||||
|
||||
entity_description: ImageEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
description: ImageEntityDescription,
|
||||
unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize the image."""
|
||||
super().__init__(coordinator, name, unique_id)
|
||||
ImageEntity.__init__(self, hass)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{unique_id}-{description.key}"
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update image attributes."""
|
||||
image_data = self.get_aemet_value([self.entity_description.key])
|
||||
self._cached_image = Image(
|
||||
content_type=image_data.get(AOD_IMG_TYPE),
|
||||
content=image_data.get(AOD_IMG_BYTES),
|
||||
)
|
||||
self._attr_image_last_updated = image_data.get(AOD_DATETIME)
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.6.3"]
|
||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||
}
|
||||
|
||||
@@ -18,10 +18,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"image": {
|
||||
"weather_radar": {
|
||||
"name": "Weather radar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"radar_updates": "Gather data from AEMET weather radar",
|
||||
"station_updates": "Gather data from AEMET weather stations"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ rules:
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options to configure
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
@@ -41,12 +43,16 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not require authentication.
|
||||
test-coverage: done
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: DHCP is still possible
|
||||
discovery:
|
||||
status: todo
|
||||
comment: DHCP is still possible
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
},
|
||||
"enable_motion_recording": {
|
||||
"name": "Enables motion recording",
|
||||
"name": "Enable motion recording",
|
||||
"description": "Enables recording a clip to camera storage when motion is detected.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
@@ -51,8 +51,8 @@
|
||||
}
|
||||
},
|
||||
"disable_motion_recording": {
|
||||
"name": "Disables motion recording",
|
||||
"description": "Disable recording a clip to camera storage when motion is detected.",
|
||||
"name": "Disable motion recording",
|
||||
"description": "Disables recording a clip to camera storage when motion is detected.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
|
||||
|
||||
@@ -11,12 +11,7 @@ from python_homeassistant_analytics import (
|
||||
from python_homeassistant_analytics.models import IntegrationType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -25,6 +20,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
)
|
||||
|
||||
from . import AnalyticsInsightsConfigEntry
|
||||
from .const import (
|
||||
CONF_TRACKED_ADDONS,
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
@@ -46,7 +42,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AnalyticsInsightsConfigEntry,
|
||||
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return HomeassistantAnalyticsOptionsFlowHandler()
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_homeassistant_analytics"],
|
||||
"requirements": ["python-homeassistant-analytics==0.8.0"],
|
||||
"requirements": ["python-homeassistant-analytics==0.8.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
100
homeassistant/components/analytics_insights/quality_scale.yaml
Normal file
100
homeassistant/components/analytics_insights/quality_scale.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: todo
|
||||
docs-installation-instructions: todo
|
||||
docs-removal-instructions: todo
|
||||
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: |
|
||||
This integration does not provide actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
The coordinator handles this.
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
The coordinator handles this.
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not require authentication.
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is a cloud service and thus does not support discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is a cloud service and thus does not support discovery.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has a fixed single service.
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not have entities with device classes.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: All the options of this integration are managed via the options flow
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have any cases where raising an issue is needed.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has a fixed single service.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -156,7 +156,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# and one of them, which could end up being in discovery_info.host, is from a
|
||||
# different device. If any of the discovery_info.ip_addresses matches the
|
||||
# existing host, don't update the host.
|
||||
if existing_config_entry and len(discovery_info.ip_addresses) > 1:
|
||||
if (
|
||||
existing_config_entry
|
||||
# Ignored entries don't have host
|
||||
and CONF_HOST in existing_config_entry.data
|
||||
and len(discovery_info.ip_addresses) > 1
|
||||
):
|
||||
existing_host = existing_config_entry.data[CONF_HOST]
|
||||
if existing_host != self.host:
|
||||
if existing_host in [
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["apprise"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["apprise==1.9.0"]
|
||||
"requirements": ["apprise==1.9.1"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -20,7 +22,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
add_entities([ApSystemsMaxOutputNumber(config_entry.runtime_data)])
|
||||
add_entities([ApSystemsMaxOutputNumber(config_entry.runtime_data)], True)
|
||||
|
||||
|
||||
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
@@ -45,7 +47,13 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Set the state with the value fetched from the inverter."""
|
||||
self._attr_native_value = await self._api.get_max_power()
|
||||
try:
|
||||
status = await self._api.get_max_power()
|
||||
except (TimeoutError, ClientConnectorError):
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._attr_native_value = status
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the desired output power."""
|
||||
|
||||
@@ -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==2.5.5"]
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.6"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
# Pre-import backup to avoid it being imported
|
||||
# later when the import executor is busy and delaying
|
||||
# startup
|
||||
from . import backup # noqa: F401
|
||||
from .agent import (
|
||||
BackupAgent,
|
||||
BackupAgentError,
|
||||
|
||||
@@ -33,8 +33,8 @@ class StoredBackupConfig(TypedDict):
|
||||
"""Represent the stored backup config."""
|
||||
|
||||
create_backup: StoredCreateBackupConfig
|
||||
last_attempted_strategy_backup: str | None
|
||||
last_completed_strategy_backup: str | None
|
||||
last_attempted_automatic_backup: str | None
|
||||
last_completed_automatic_backup: str | None
|
||||
retention: StoredRetentionConfig
|
||||
schedule: StoredBackupSchedule
|
||||
|
||||
@@ -44,8 +44,8 @@ class BackupConfigData:
|
||||
"""Represent loaded backup config data."""
|
||||
|
||||
create_backup: CreateBackupConfig
|
||||
last_attempted_strategy_backup: datetime | None = None
|
||||
last_completed_strategy_backup: datetime | None = None
|
||||
last_attempted_automatic_backup: datetime | None = None
|
||||
last_completed_automatic_backup: datetime | None = None
|
||||
retention: RetentionConfig
|
||||
schedule: BackupSchedule
|
||||
|
||||
@@ -59,12 +59,12 @@ class BackupConfigData:
|
||||
include_folders = None
|
||||
retention = data["retention"]
|
||||
|
||||
if last_attempted_str := data["last_attempted_strategy_backup"]:
|
||||
if last_attempted_str := data["last_attempted_automatic_backup"]:
|
||||
last_attempted = dt_util.parse_datetime(last_attempted_str)
|
||||
else:
|
||||
last_attempted = None
|
||||
|
||||
if last_attempted_str := data["last_completed_strategy_backup"]:
|
||||
if last_attempted_str := data["last_completed_automatic_backup"]:
|
||||
last_completed = dt_util.parse_datetime(last_attempted_str)
|
||||
else:
|
||||
last_completed = None
|
||||
@@ -79,8 +79,8 @@ class BackupConfigData:
|
||||
name=data["create_backup"]["name"],
|
||||
password=data["create_backup"]["password"],
|
||||
),
|
||||
last_attempted_strategy_backup=last_attempted,
|
||||
last_completed_strategy_backup=last_completed,
|
||||
last_attempted_automatic_backup=last_attempted,
|
||||
last_completed_automatic_backup=last_completed,
|
||||
retention=RetentionConfig(
|
||||
copies=retention["copies"],
|
||||
days=retention["days"],
|
||||
@@ -90,20 +90,20 @@ class BackupConfigData:
|
||||
|
||||
def to_dict(self) -> StoredBackupConfig:
|
||||
"""Convert backup config data to a dict."""
|
||||
if self.last_attempted_strategy_backup:
|
||||
last_attempted = self.last_attempted_strategy_backup.isoformat()
|
||||
if self.last_attempted_automatic_backup:
|
||||
last_attempted = self.last_attempted_automatic_backup.isoformat()
|
||||
else:
|
||||
last_attempted = None
|
||||
|
||||
if self.last_completed_strategy_backup:
|
||||
last_completed = self.last_completed_strategy_backup.isoformat()
|
||||
if self.last_completed_automatic_backup:
|
||||
last_completed = self.last_completed_automatic_backup.isoformat()
|
||||
else:
|
||||
last_completed = None
|
||||
|
||||
return StoredBackupConfig(
|
||||
create_backup=self.create_backup.to_dict(),
|
||||
last_attempted_strategy_backup=last_attempted,
|
||||
last_completed_strategy_backup=last_completed,
|
||||
last_attempted_automatic_backup=last_attempted,
|
||||
last_completed_automatic_backup=last_completed,
|
||||
retention=self.retention.to_dict(),
|
||||
schedule=self.schedule.to_dict(),
|
||||
)
|
||||
@@ -286,7 +286,7 @@ class BackupSchedule:
|
||||
self._unschedule_next(manager)
|
||||
now = dt_util.now()
|
||||
if (cron_event := self.cron_event) is None:
|
||||
seed_time = manager.config.data.last_completed_strategy_backup or now
|
||||
seed_time = manager.config.data.last_completed_automatic_backup or now
|
||||
cron_event = self.cron_event = CronSim(cron_pattern, seed_time)
|
||||
next_time = next(cron_event)
|
||||
|
||||
@@ -316,7 +316,7 @@ class BackupSchedule:
|
||||
include_homeassistant=True, # always include HA
|
||||
name=config_data.create_backup.name,
|
||||
password=config_data.create_backup.password,
|
||||
with_strategy_settings=True,
|
||||
with_automatic_settings=True,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
# another more specific exception will be added
|
||||
@@ -404,14 +404,14 @@ async def _delete_filtered_backups(
|
||||
get_agent_errors,
|
||||
)
|
||||
|
||||
# only delete backups that are created by the backup strategy
|
||||
# only delete backups that are created with the saved automatic settings
|
||||
backups = {
|
||||
backup_id: backup
|
||||
for backup_id, backup in backups.items()
|
||||
if backup.with_strategy_settings
|
||||
if backup.with_automatic_settings
|
||||
}
|
||||
|
||||
LOGGER.debug("Total strategy backups: %s", backups)
|
||||
LOGGER.debug("Total automatic backups: %s", backups)
|
||||
|
||||
filtered_backups = backup_filter(backups)
|
||||
|
||||
@@ -467,7 +467,7 @@ async def delete_backups_exceeding_configured_count(manager: BackupManager) -> N
|
||||
sorted(
|
||||
backups.items(),
|
||||
key=lambda backup_item: backup_item[1].date,
|
||||
)[: len(backups) - manager.config.data.retention.copies]
|
||||
)[: max(len(backups) - manager.config.data.retention.copies, 0)]
|
||||
)
|
||||
|
||||
await _delete_filtered_backups(manager, _backups_filter)
|
||||
|
||||
@@ -23,7 +23,11 @@ from homeassistant.backup_restore import RESTORE_BACKUP_FILE, password_to_key
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import integration_platform
|
||||
from homeassistant.helpers import (
|
||||
instance_id,
|
||||
integration_platform,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -44,7 +48,11 @@ from .const import (
|
||||
)
|
||||
from .models import AgentBackup, Folder
|
||||
from .store import BackupStore
|
||||
from .util import make_backup_dir, read_backup
|
||||
from .util import make_backup_dir, read_backup, validate_password
|
||||
|
||||
|
||||
class IncorrectPasswordError(HomeAssistantError):
|
||||
"""Raised when the password is incorrect."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
@@ -60,7 +68,7 @@ class ManagerBackup(AgentBackup):
|
||||
|
||||
agent_ids: list[str]
|
||||
failed_agent_ids: list[str]
|
||||
with_strategy_settings: bool
|
||||
with_automatic_settings: bool | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
@@ -200,6 +208,7 @@ class BackupReaderWriter(abc.ABC):
|
||||
*,
|
||||
agent_ids: list[str],
|
||||
backup_name: str,
|
||||
extra_metadata: dict[str, bool | str],
|
||||
include_addons: list[str] | None,
|
||||
include_all_addons: bool,
|
||||
include_database: bool,
|
||||
@@ -445,16 +454,18 @@ class BackupManager:
|
||||
if (backup_id := agent_backup.backup_id) not in backups:
|
||||
if known_backup := self.known_backups.get(backup_id):
|
||||
failed_agent_ids = known_backup.failed_agent_ids
|
||||
with_strategy_settings = known_backup.with_strategy_settings
|
||||
else:
|
||||
failed_agent_ids = []
|
||||
with_strategy_settings = False
|
||||
with_automatic_settings = self.is_our_automatic_backup(
|
||||
agent_backup, await instance_id.async_get(self.hass)
|
||||
)
|
||||
backups[backup_id] = ManagerBackup(
|
||||
agent_ids=[],
|
||||
addons=agent_backup.addons,
|
||||
backup_id=backup_id,
|
||||
date=agent_backup.date,
|
||||
database_included=agent_backup.database_included,
|
||||
extra_metadata=agent_backup.extra_metadata,
|
||||
failed_agent_ids=failed_agent_ids,
|
||||
folders=agent_backup.folders,
|
||||
homeassistant_included=agent_backup.homeassistant_included,
|
||||
@@ -462,7 +473,7 @@ class BackupManager:
|
||||
name=agent_backup.name,
|
||||
protected=agent_backup.protected,
|
||||
size=agent_backup.size,
|
||||
with_strategy_settings=with_strategy_settings,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
backups[backup_id].agent_ids.append(agent_ids[idx])
|
||||
|
||||
@@ -494,16 +505,18 @@ class BackupManager:
|
||||
if backup is None:
|
||||
if known_backup := self.known_backups.get(backup_id):
|
||||
failed_agent_ids = known_backup.failed_agent_ids
|
||||
with_strategy_settings = known_backup.with_strategy_settings
|
||||
else:
|
||||
failed_agent_ids = []
|
||||
with_strategy_settings = False
|
||||
with_automatic_settings = self.is_our_automatic_backup(
|
||||
result, await instance_id.async_get(self.hass)
|
||||
)
|
||||
backup = ManagerBackup(
|
||||
agent_ids=[],
|
||||
addons=result.addons,
|
||||
backup_id=result.backup_id,
|
||||
date=result.date,
|
||||
database_included=result.database_included,
|
||||
extra_metadata=result.extra_metadata,
|
||||
failed_agent_ids=failed_agent_ids,
|
||||
folders=result.folders,
|
||||
homeassistant_included=result.homeassistant_included,
|
||||
@@ -511,12 +524,28 @@ class BackupManager:
|
||||
name=result.name,
|
||||
protected=result.protected,
|
||||
size=result.size,
|
||||
with_strategy_settings=with_strategy_settings,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
backup.agent_ids.append(agent_ids[idx])
|
||||
|
||||
return (backup, agent_errors)
|
||||
|
||||
@staticmethod
|
||||
def is_our_automatic_backup(
|
||||
backup: AgentBackup, our_instance_id: str
|
||||
) -> bool | None:
|
||||
"""Check if a backup was created by us and return automatic_settings flag.
|
||||
|
||||
Returns `None` if the backup was not created by us, or if the
|
||||
automatic_settings flag is not a boolean.
|
||||
"""
|
||||
if backup.extra_metadata.get("instance_id") != our_instance_id:
|
||||
return None
|
||||
with_automatic_settings = backup.extra_metadata.get("with_automatic_settings")
|
||||
if not isinstance(with_automatic_settings, bool):
|
||||
return None
|
||||
return with_automatic_settings
|
||||
|
||||
async def async_delete_backup(self, backup_id: str) -> dict[str, Exception]:
|
||||
"""Delete a backup."""
|
||||
agent_errors: dict[str, Exception] = {}
|
||||
@@ -598,7 +627,7 @@ class BackupManager:
|
||||
open_stream=written_backup.open_stream,
|
||||
)
|
||||
await written_backup.release_stream()
|
||||
self.known_backups.add(written_backup.backup, agent_errors, False)
|
||||
self.known_backups.add(written_backup.backup, agent_errors)
|
||||
|
||||
async def async_create_backup(
|
||||
self,
|
||||
@@ -611,7 +640,7 @@ class BackupManager:
|
||||
include_homeassistant: bool,
|
||||
name: str | None,
|
||||
password: str | None,
|
||||
with_strategy_settings: bool = False,
|
||||
with_automatic_settings: bool = False,
|
||||
) -> NewBackup:
|
||||
"""Create a backup."""
|
||||
new_backup = await self.async_initiate_backup(
|
||||
@@ -623,7 +652,7 @@ class BackupManager:
|
||||
include_homeassistant=include_homeassistant,
|
||||
name=name,
|
||||
password=password,
|
||||
with_strategy_settings=with_strategy_settings,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
assert self._backup_finish_task
|
||||
await self._backup_finish_task
|
||||
@@ -640,14 +669,14 @@ class BackupManager:
|
||||
include_homeassistant: bool,
|
||||
name: str | None,
|
||||
password: str | None,
|
||||
with_strategy_settings: bool = False,
|
||||
with_automatic_settings: bool = False,
|
||||
) -> NewBackup:
|
||||
"""Initiate generating a backup."""
|
||||
if self.state is not BackupManagerState.IDLE:
|
||||
raise HomeAssistantError(f"Backup manager busy: {self.state}")
|
||||
|
||||
if with_strategy_settings:
|
||||
self.config.data.last_attempted_strategy_backup = dt_util.now()
|
||||
if with_automatic_settings:
|
||||
self.config.data.last_attempted_automatic_backup = dt_util.now()
|
||||
self.store.save()
|
||||
|
||||
self.async_on_backup_event(
|
||||
@@ -663,13 +692,15 @@ class BackupManager:
|
||||
include_homeassistant=include_homeassistant,
|
||||
name=name,
|
||||
password=password,
|
||||
with_strategy_settings=with_strategy_settings,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
except Exception:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
self.async_on_backup_event(IdleEvent())
|
||||
if with_automatic_settings:
|
||||
self._update_issue_backup_failed()
|
||||
raise
|
||||
|
||||
async def _async_create_backup(
|
||||
@@ -683,7 +714,7 @@ class BackupManager:
|
||||
include_homeassistant: bool,
|
||||
name: str | None,
|
||||
password: str | None,
|
||||
with_strategy_settings: bool,
|
||||
with_automatic_settings: bool,
|
||||
) -> NewBackup:
|
||||
"""Initiate generating a backup."""
|
||||
if not agent_ids:
|
||||
@@ -695,10 +726,17 @@ class BackupManager:
|
||||
"Cannot include all addons and specify specific addons"
|
||||
)
|
||||
|
||||
backup_name = name or f"Core {HAVERSION}"
|
||||
backup_name = (
|
||||
name
|
||||
or f"{"Automatic" if with_automatic_settings else "Custom"} {HAVERSION}"
|
||||
)
|
||||
new_backup, self._backup_task = await self._reader_writer.async_create_backup(
|
||||
agent_ids=agent_ids,
|
||||
backup_name=backup_name,
|
||||
extra_metadata={
|
||||
"instance_id": await instance_id.async_get(self.hass),
|
||||
"with_automatic_settings": with_automatic_settings,
|
||||
},
|
||||
include_addons=include_addons,
|
||||
include_all_addons=include_all_addons,
|
||||
include_database=include_database,
|
||||
@@ -708,13 +746,13 @@ class BackupManager:
|
||||
password=password,
|
||||
)
|
||||
self._backup_finish_task = self.hass.async_create_task(
|
||||
self._async_finish_backup(agent_ids, with_strategy_settings),
|
||||
self._async_finish_backup(agent_ids, with_automatic_settings),
|
||||
name="backup_manager_finish_backup",
|
||||
)
|
||||
return new_backup
|
||||
|
||||
async def _async_finish_backup(
|
||||
self, agent_ids: list[str], with_strategy_settings: bool
|
||||
self, agent_ids: list[str], with_automatic_settings: bool
|
||||
) -> None:
|
||||
if TYPE_CHECKING:
|
||||
assert self._backup_task is not None
|
||||
@@ -725,6 +763,8 @@ class BackupManager:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
if with_automatic_settings:
|
||||
self._update_issue_backup_failed()
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Generated new backup with backup_id %s, uploading to agents %s",
|
||||
@@ -743,13 +783,12 @@ class BackupManager:
|
||||
open_stream=written_backup.open_stream,
|
||||
)
|
||||
await written_backup.release_stream()
|
||||
if with_strategy_settings:
|
||||
# create backup was successful, update last_completed_strategy_backup
|
||||
self.config.data.last_completed_strategy_backup = dt_util.now()
|
||||
if with_automatic_settings:
|
||||
# create backup was successful, update last_completed_automatic_backup
|
||||
self.config.data.last_completed_automatic_backup = dt_util.now()
|
||||
self.store.save()
|
||||
self.known_backups.add(
|
||||
written_backup.backup, agent_errors, with_strategy_settings
|
||||
)
|
||||
self._update_issue_after_agent_upload(agent_errors)
|
||||
self.known_backups.add(written_backup.backup, agent_errors)
|
||||
|
||||
# delete old backups more numerous than copies
|
||||
await delete_backups_exceeding_configured_count(self)
|
||||
@@ -855,6 +894,38 @@ class BackupManager:
|
||||
self._backup_event_subscriptions.append(on_event)
|
||||
return remove_subscription
|
||||
|
||||
def _update_issue_backup_failed(self) -> None:
|
||||
"""Update issue registry when a backup fails."""
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"automatic_backup_failed",
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
learn_more_url="homeassistant://config/backup",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="automatic_backup_failed_create",
|
||||
)
|
||||
|
||||
def _update_issue_after_agent_upload(
|
||||
self, agent_errors: dict[str, Exception]
|
||||
) -> None:
|
||||
"""Update issue registry after a backup is uploaded to agents."""
|
||||
if not agent_errors:
|
||||
ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed")
|
||||
return
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"automatic_backup_failed",
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
learn_more_url="homeassistant://config/backup",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="automatic_backup_failed_upload_agents",
|
||||
translation_placeholders={"failed_agents": ", ".join(agent_errors)},
|
||||
)
|
||||
|
||||
|
||||
class KnownBackups:
|
||||
"""Track known backups."""
|
||||
@@ -870,7 +941,6 @@ class KnownBackups:
|
||||
backup["backup_id"]: KnownBackup(
|
||||
backup_id=backup["backup_id"],
|
||||
failed_agent_ids=backup["failed_agent_ids"],
|
||||
with_strategy_settings=backup["with_strategy_settings"],
|
||||
)
|
||||
for backup in stored_backups
|
||||
}
|
||||
@@ -883,13 +953,11 @@ class KnownBackups:
|
||||
self,
|
||||
backup: AgentBackup,
|
||||
agent_errors: dict[str, Exception],
|
||||
with_strategy_settings: bool,
|
||||
) -> None:
|
||||
"""Add a backup."""
|
||||
self._backups[backup.backup_id] = KnownBackup(
|
||||
backup_id=backup.backup_id,
|
||||
failed_agent_ids=list(agent_errors),
|
||||
with_strategy_settings=with_strategy_settings,
|
||||
)
|
||||
self._manager.store.save()
|
||||
|
||||
@@ -911,14 +979,12 @@ class KnownBackup:
|
||||
|
||||
backup_id: str
|
||||
failed_agent_ids: list[str]
|
||||
with_strategy_settings: bool
|
||||
|
||||
def to_dict(self) -> StoredKnownBackup:
|
||||
"""Convert known backup to a dict."""
|
||||
return {
|
||||
"backup_id": self.backup_id,
|
||||
"failed_agent_ids": self.failed_agent_ids,
|
||||
"with_strategy_settings": self.with_strategy_settings,
|
||||
}
|
||||
|
||||
|
||||
@@ -927,7 +993,6 @@ class StoredKnownBackup(TypedDict):
|
||||
|
||||
backup_id: str
|
||||
failed_agent_ids: list[str]
|
||||
with_strategy_settings: bool
|
||||
|
||||
|
||||
class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
@@ -945,6 +1010,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
*,
|
||||
agent_ids: list[str],
|
||||
backup_name: str,
|
||||
extra_metadata: dict[str, bool | str],
|
||||
include_addons: list[str] | None,
|
||||
include_all_addons: bool,
|
||||
include_database: bool,
|
||||
@@ -969,6 +1035,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
agent_ids=agent_ids,
|
||||
backup_id=backup_id,
|
||||
backup_name=backup_name,
|
||||
extra_metadata=extra_metadata,
|
||||
include_database=include_database,
|
||||
date_str=date_str,
|
||||
on_progress=on_progress,
|
||||
@@ -987,6 +1054,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
backup_id: str,
|
||||
backup_name: str,
|
||||
date_str: str,
|
||||
extra_metadata: dict[str, bool | str],
|
||||
include_database: bool,
|
||||
on_progress: Callable[[ManagerStateEvent], None],
|
||||
password: str | None,
|
||||
@@ -1012,6 +1080,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
backup_data = {
|
||||
"compressed": True,
|
||||
"date": date_str,
|
||||
"extra": extra_metadata,
|
||||
"homeassistant": {
|
||||
"exclude_database": not include_database,
|
||||
"version": HAVERSION,
|
||||
@@ -1035,6 +1104,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
backup_id=backup_id,
|
||||
database_included=include_database,
|
||||
date=date_str,
|
||||
extra_metadata=extra_metadata,
|
||||
folders=[],
|
||||
homeassistant_included=True,
|
||||
homeassistant_version=HAVERSION,
|
||||
@@ -1206,6 +1276,12 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
|
||||
remove_after_restore = True
|
||||
|
||||
password_valid = await self._hass.async_add_executor_job(
|
||||
validate_password, path, password
|
||||
)
|
||||
if not password_valid:
|
||||
raise IncorrectPasswordError("The password provided is incorrect.")
|
||||
|
||||
def _write_restore_file() -> None:
|
||||
"""Write the restore file."""
|
||||
Path(self._hass.config.path(RESTORE_BACKUP_FILE)).write_text(
|
||||
|
||||
@@ -33,6 +33,7 @@ class AgentBackup:
|
||||
backup_id: str
|
||||
date: str
|
||||
database_included: bool
|
||||
extra_metadata: dict[str, bool | str]
|
||||
folders: list[Folder]
|
||||
homeassistant_included: bool
|
||||
homeassistant_version: str | None # None if homeassistant_included is False
|
||||
@@ -44,6 +45,12 @@ class AgentBackup:
|
||||
"""Return a dict representation of this backup."""
|
||||
return asdict(self)
|
||||
|
||||
def as_frontend_json(self) -> dict:
|
||||
"""Return a dict representation of this backup for sending to frontend."""
|
||||
return {
|
||||
key: val for key, val in asdict(self).items() if key != "extra_metadata"
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||
"""Create an instance from a JSON serialization."""
|
||||
@@ -52,6 +59,7 @@ class AgentBackup:
|
||||
backup_id=data["backup_id"],
|
||||
date=data["date"],
|
||||
database_included=data["database_included"],
|
||||
extra_metadata=data["extra_metadata"],
|
||||
folders=[Folder(folder) for folder in data["folders"]],
|
||||
homeassistant_included=data["homeassistant_included"],
|
||||
homeassistant_version=data["homeassistant_version"],
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"automatic_backup_failed_create": {
|
||||
"title": "Automatic backup could not be created",
|
||||
"description": "The automatic backup could not be created. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
|
||||
},
|
||||
"automatic_backup_failed_upload_agents": {
|
||||
"title": "Automatic backup could not be uploaded to agents",
|
||||
"description": "The automatic backup could not be uploaded to agents {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"create": {
|
||||
"name": "Create backup",
|
||||
|
||||
@@ -9,11 +9,13 @@ import tarfile
|
||||
from typing import cast
|
||||
|
||||
import aiohttp
|
||||
from securetar import SecureTarFile
|
||||
|
||||
from homeassistant.backup_restore import password_to_key
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .const import BUF_SIZE
|
||||
from .const import BUF_SIZE, LOGGER
|
||||
from .models import AddonInfo, AgentBackup, Folder
|
||||
|
||||
|
||||
@@ -50,6 +52,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
if (
|
||||
homeassistant := cast(JsonObjectType, data.get("homeassistant"))
|
||||
) and "version" in homeassistant:
|
||||
homeassistant_included = True
|
||||
homeassistant_version = cast(str, homeassistant["version"])
|
||||
database_included = not cast(
|
||||
bool, homeassistant.get("exclude_database", False)
|
||||
@@ -60,6 +63,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
backup_id=cast(str, data["slug"]),
|
||||
database_included=database_included,
|
||||
date=cast(str, data["date"]),
|
||||
extra_metadata=cast(dict[str, bool | str], data.get("extra", {})),
|
||||
folders=folders,
|
||||
homeassistant_included=homeassistant_included,
|
||||
homeassistant_version=homeassistant_version,
|
||||
@@ -69,6 +73,39 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
)
|
||||
|
||||
|
||||
def validate_password(path: Path, password: str | None) -> bool:
|
||||
"""Validate the password."""
|
||||
with tarfile.open(path, "r:", bufsize=BUF_SIZE) as backup_file:
|
||||
compressed = False
|
||||
ha_tar_name = "homeassistant.tar"
|
||||
try:
|
||||
ha_tar = backup_file.extractfile(ha_tar_name)
|
||||
except KeyError:
|
||||
compressed = True
|
||||
ha_tar_name = "homeassistant.tar.gz"
|
||||
try:
|
||||
ha_tar = backup_file.extractfile(ha_tar_name)
|
||||
except KeyError:
|
||||
LOGGER.error("No homeassistant.tar or homeassistant.tar.gz found")
|
||||
return False
|
||||
try:
|
||||
with SecureTarFile(
|
||||
path, # Not used
|
||||
gzip=compressed,
|
||||
key=password_to_key(password) if password is not None else None,
|
||||
mode="r",
|
||||
fileobj=ha_tar,
|
||||
):
|
||||
# If we can read the tar file, the password is correct
|
||||
return True
|
||||
except tarfile.ReadError:
|
||||
LOGGER.debug("Invalid password")
|
||||
return False
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error validating password")
|
||||
return False
|
||||
|
||||
|
||||
async def receive_file(
|
||||
hass: HomeAssistant, contents: aiohttp.BodyPartReader, path: Path
|
||||
) -> None:
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .config import ScheduleState
|
||||
from .const import DATA_MANAGER, LOGGER
|
||||
from .manager import ManagerStateEvent
|
||||
from .manager import IncorrectPasswordError, ManagerStateEvent
|
||||
from .models import Folder
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
|
||||
websocket_api.async_register_command(hass, handle_details)
|
||||
websocket_api.async_register_command(hass, handle_info)
|
||||
websocket_api.async_register_command(hass, handle_create)
|
||||
websocket_api.async_register_command(hass, handle_create_with_strategy_settings)
|
||||
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
|
||||
websocket_api.async_register_command(hass, handle_delete)
|
||||
websocket_api.async_register_command(hass, handle_restore)
|
||||
websocket_api.async_register_command(hass, handle_subscribe_events)
|
||||
@@ -51,9 +51,9 @@ async def handle_info(
|
||||
"agent_errors": {
|
||||
agent_id: str(err) for agent_id, err in agent_errors.items()
|
||||
},
|
||||
"backups": list(backups.values()),
|
||||
"last_attempted_strategy_backup": manager.config.data.last_attempted_strategy_backup,
|
||||
"last_completed_strategy_backup": manager.config.data.last_completed_strategy_backup,
|
||||
"backups": [backup.as_frontend_json() for backup in backups.values()],
|
||||
"last_attempted_automatic_backup": manager.config.data.last_attempted_automatic_backup,
|
||||
"last_completed_automatic_backup": manager.config.data.last_completed_automatic_backup,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ async def handle_details(
|
||||
"agent_errors": {
|
||||
agent_id: str(err) for agent_id, err in agent_errors.items()
|
||||
},
|
||||
"backup": backup,
|
||||
"backup": backup.as_frontend_json() if backup else None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -131,16 +131,20 @@ async def handle_restore(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Restore a backup."""
|
||||
await hass.data[DATA_MANAGER].async_restore_backup(
|
||||
msg["backup_id"],
|
||||
agent_id=msg["agent_id"],
|
||||
password=msg.get("password"),
|
||||
restore_addons=msg.get("restore_addons"),
|
||||
restore_database=msg["restore_database"],
|
||||
restore_folders=msg.get("restore_folders"),
|
||||
restore_homeassistant=msg["restore_homeassistant"],
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
try:
|
||||
await hass.data[DATA_MANAGER].async_restore_backup(
|
||||
msg["backup_id"],
|
||||
agent_id=msg["agent_id"],
|
||||
password=msg.get("password"),
|
||||
restore_addons=msg.get("restore_addons"),
|
||||
restore_database=msg["restore_database"],
|
||||
restore_folders=msg.get("restore_folders"),
|
||||
restore_homeassistant=msg["restore_homeassistant"],
|
||||
)
|
||||
except IncorrectPasswordError:
|
||||
connection.send_error(msg["id"], "password_incorrect", "Incorrect password")
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@@ -181,11 +185,11 @@ async def handle_create(
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/generate_with_strategy_settings",
|
||||
vol.Required("type"): "backup/generate_with_automatic_settings",
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def handle_create_with_strategy_settings(
|
||||
async def handle_create_with_automatic_settings(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -202,7 +206,7 @@ async def handle_create_with_strategy_settings(
|
||||
include_homeassistant=True, # always include HA
|
||||
name=config_data.create_backup.name,
|
||||
password=config_data.create_backup.password,
|
||||
with_strategy_settings=True,
|
||||
with_automatic_settings=True,
|
||||
)
|
||||
connection.send_result(msg["id"], backup)
|
||||
|
||||
@@ -291,11 +295,15 @@ async def handle_config_info(
|
||||
vol.Required("type"): "backup/config/update",
|
||||
vol.Optional("create_backup"): vol.Schema(
|
||||
{
|
||||
vol.Optional("agent_ids"): vol.All(list[str]),
|
||||
vol.Optional("include_addons"): vol.Any(list[str], None),
|
||||
vol.Optional("agent_ids"): vol.All([str], vol.Unique()),
|
||||
vol.Optional("include_addons"): vol.Any(
|
||||
vol.All([str], vol.Unique()), None
|
||||
),
|
||||
vol.Optional("include_all_addons"): bool,
|
||||
vol.Optional("include_database"): bool,
|
||||
vol.Optional("include_folders"): vol.Any([vol.Coerce(Folder)], None),
|
||||
vol.Optional("include_folders"): vol.Any(
|
||||
vol.All([vol.Coerce(Folder)], vol.Unique()), None
|
||||
),
|
||||
vol.Optional("name"): vol.Any(str, None),
|
||||
vol.Optional("password"): vol.Any(str, None),
|
||||
},
|
||||
|
||||
@@ -84,16 +84,16 @@
|
||||
}
|
||||
},
|
||||
"send_pin": {
|
||||
"name": "Send pin",
|
||||
"description": "Sends a new PIN to blink for 2FA.",
|
||||
"name": "Send PIN",
|
||||
"description": "Sends a new PIN to Blink for 2FA.",
|
||||
"fields": {
|
||||
"pin": {
|
||||
"name": "Pin",
|
||||
"description": "PIN received from blink. Leave empty if you only received a verification email."
|
||||
"name": "PIN",
|
||||
"description": "PIN received from Blink. Leave empty if you only received a verification email."
|
||||
},
|
||||
"config_entry_id": {
|
||||
"name": "Integration ID",
|
||||
"description": "The Blink Integration id."
|
||||
"description": "The Blink Integration ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,9 +103,10 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
_existing_entry_data: Mapping[str, Any] | None = None
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
self._existing_entry_data: dict[str, Any] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -175,19 +176,15 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the change password step."""
|
||||
existing_data = (
|
||||
dict(self._existing_entry_data) if self._existing_entry_data else {}
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
return await self.async_step_user(existing_data | user_input)
|
||||
return await self.async_step_user(self._existing_entry_data | user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="change_password",
|
||||
data_schema=RECONFIGURE_SCHEMA,
|
||||
description_placeholders={
|
||||
CONF_USERNAME: existing_data[CONF_USERNAME],
|
||||
CONF_REGION: existing_data[CONF_REGION],
|
||||
CONF_USERNAME: self._existing_entry_data[CONF_USERNAME],
|
||||
CONF_REGION: self._existing_entry_data[CONF_REGION],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -195,14 +192,14 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._existing_entry_data = entry_data
|
||||
self._existing_entry_data = dict(entry_data)
|
||||
return await self.async_step_change_password()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reconfiguration flow initialized by the user."""
|
||||
self._existing_entry_data = self._get_reconfigure_entry().data
|
||||
self._existing_entry_data = dict(self._get_reconfigure_entry().data)
|
||||
return await self.async_step_change_password()
|
||||
|
||||
async def async_step_captcha(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.3.9"]
|
||||
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
|
||||
}
|
||||
|
||||
85
homeassistant/components/cambridge_audio/media_browser.py
Normal file
85
homeassistant/components/cambridge_audio/media_browser.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Support for media browsing."""
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
from aiostreammagic.models import Preset
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def async_browse_media(
|
||||
hass: HomeAssistant,
|
||||
client: StreamMagicClient,
|
||||
media_content_id: str | None,
|
||||
media_content_type: str | None,
|
||||
) -> BrowseMedia:
|
||||
"""Browse media."""
|
||||
|
||||
if media_content_type == "presets":
|
||||
return await _presets_payload(client.preset_list.presets)
|
||||
|
||||
return await _root_payload(
|
||||
hass,
|
||||
client,
|
||||
)
|
||||
|
||||
|
||||
async def _root_payload(
|
||||
hass: HomeAssistant,
|
||||
client: StreamMagicClient,
|
||||
) -> BrowseMedia:
|
||||
"""Return root payload for Cambridge Audio."""
|
||||
children: list[BrowseMedia] = []
|
||||
|
||||
if client.preset_list.presets:
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title="Presets",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="presets",
|
||||
thumbnail="https://brands.home-assistant.io/_/cambridge_audio/logo.png",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
)
|
||||
|
||||
return BrowseMedia(
|
||||
title="Cambridge Audio",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="root",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
async def _presets_payload(presets: list[Preset]) -> BrowseMedia:
|
||||
"""Create payload to list presets."""
|
||||
|
||||
children: list[BrowseMedia] = []
|
||||
for preset in presets:
|
||||
if preset.state != "OK":
|
||||
continue
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title=preset.name,
|
||||
media_class=MediaClass.MUSIC,
|
||||
media_content_id=str(preset.preset_id),
|
||||
media_content_type="preset",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=preset.art_url,
|
||||
)
|
||||
)
|
||||
|
||||
return BrowseMedia(
|
||||
title="Presets",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="presets",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
@@ -13,6 +13,7 @@ from aiostreammagic import (
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
@@ -24,7 +25,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import CambridgeAudioConfigEntry
|
||||
from . import CambridgeAudioConfigEntry, media_browser
|
||||
from .const import (
|
||||
CAMBRIDGE_MEDIA_TYPE_AIRABLE,
|
||||
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
|
||||
@@ -34,7 +35,8 @@ from .const import (
|
||||
from .entity import CambridgeAudioEntity, command
|
||||
|
||||
BASE_FEATURES = (
|
||||
MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
@@ -338,3 +340,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
|
||||
if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
|
||||
await self.client.play_radio_url("Radio", media_id)
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""Implement the media browsing helper."""
|
||||
return await media_browser.async_browse_media(
|
||||
self.hass, self.client, media_content_id, media_content_type
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
@@ -129,7 +129,7 @@ class ChromecastInfo:
|
||||
class ChromeCastZeroconf:
|
||||
"""Class to hold a zeroconf instance."""
|
||||
|
||||
__zconf: zeroconf.HaZeroconf | None = None
|
||||
__zconf: ClassVar[zeroconf.HaZeroconf | None] = None
|
||||
|
||||
@classmethod
|
||||
def set_zeroconf(cls, zconf: zeroconf.HaZeroconf) -> None:
|
||||
|
||||
@@ -240,6 +240,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"preset_mode",
|
||||
"preset_modes",
|
||||
"is_aux_heat",
|
||||
"is_on",
|
||||
"fan_mode",
|
||||
"fan_modes",
|
||||
"swing_mode",
|
||||
@@ -280,6 +281,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
_attr_hvac_mode: HVACMode | None
|
||||
_attr_hvac_modes: list[HVACMode]
|
||||
_attr_is_aux_heat: bool | None
|
||||
_attr_is_on: bool | None
|
||||
_attr_max_humidity: float = DEFAULT_MAX_HUMIDITY
|
||||
_attr_max_temp: float
|
||||
_attr_min_humidity: float = DEFAULT_MIN_HUMIDITY
|
||||
@@ -352,11 +354,33 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
hvac_mode = self.hvac_mode
|
||||
if hvac_mode is None:
|
||||
return None
|
||||
if hasattr(self, "_attr_is_on") and self._attr_is_on is False:
|
||||
return HVACMode.OFF.value
|
||||
# Support hvac_mode as string for custom integration backwards compatibility
|
||||
if not isinstance(hvac_mode, HVACMode):
|
||||
return HVACMode(hvac_mode).value # type: ignore[unreachable]
|
||||
return hvac_mode.value
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return True if the climate is turned on.
|
||||
|
||||
The climate's on/off state can be be controlled independently
|
||||
from the hvac_action and hvac_mode if the _attr_is_on attribute is set.
|
||||
|
||||
If the _attr_is_on attribute is set, then return that value.
|
||||
Otherwise, return True if hvac_action is not None and not HVACAction.OFF.
|
||||
Return None if hvac_action is None,
|
||||
otherwise return True if hvac_mode is not HVACMode.OFF.
|
||||
"""
|
||||
if hasattr(self, "_attr_is_on"):
|
||||
return self._attr_is_on
|
||||
if self.hvac_action is not None:
|
||||
return self.hvac_action != HVACAction.OFF
|
||||
if self.hvac_mode is None:
|
||||
return None
|
||||
return self.hvac_mode != HVACMode.OFF
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the system."""
|
||||
|
||||
@@ -36,7 +36,14 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
from . import account_link, http_api
|
||||
# Pre-import backup to avoid it being imported
|
||||
# later when the import executor is busy and delaying
|
||||
# startup
|
||||
from . import (
|
||||
account_link,
|
||||
backup, # noqa: F401
|
||||
http_api,
|
||||
)
|
||||
from .client import CloudClient
|
||||
from .const import (
|
||||
CONF_ACCOUNT_LINK_SERVER,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||
import hashlib
|
||||
from typing import Any, Self
|
||||
|
||||
@@ -18,9 +18,10 @@ from hass_nabucasa.cloud_api import (
|
||||
|
||||
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import DATA_CLOUD, DOMAIN
|
||||
from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
|
||||
|
||||
_STORAGE_BACKUP = "backup"
|
||||
|
||||
@@ -45,6 +46,31 @@ async def async_get_backup_agents(
|
||||
return [CloudBackupAgent(hass=hass, cloud=cloud)]
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_backup_agents_listener(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
listener: Callable[[], None],
|
||||
**kwargs: Any,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a listener to be called when agents are added or removed."""
|
||||
|
||||
@callback
|
||||
def unsub() -> None:
|
||||
"""Unsubscribe from events."""
|
||||
unsub_signal()
|
||||
|
||||
@callback
|
||||
def handle_event(data: Mapping[str, Any]) -> None:
|
||||
"""Handle event."""
|
||||
if data["type"] not in ("login", "logout"):
|
||||
return
|
||||
listener()
|
||||
|
||||
unsub_signal = async_dispatcher_connect(hass, EVENT_CLOUD_EVENT, handle_event)
|
||||
return unsub
|
||||
|
||||
|
||||
class ChunkAsyncStreamIterator:
|
||||
"""Async iterator for chunked streams.
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@ class CloudClient(Interface):
|
||||
},
|
||||
"version": HA_VERSION,
|
||||
"instance_id": self.prefs.instance_id,
|
||||
"name": self._hass.config.location_name,
|
||||
}
|
||||
|
||||
async def async_alexa_message(self, payload: dict[Any, Any]) -> dict[Any, Any]:
|
||||
|
||||
@@ -18,6 +18,8 @@ DATA_CLOUD: HassKey[Cloud[CloudClient]] = HassKey(DOMAIN)
|
||||
DATA_PLATFORMS_SETUP: HassKey[dict[str, asyncio.Event]] = HassKey(
|
||||
"cloud_platforms_setup"
|
||||
)
|
||||
EVENT_CLOUD_EVENT = "cloud_event"
|
||||
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
PREF_ENABLE_ALEXA = "alexa_enabled"
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util.location import async_detect_location_info
|
||||
|
||||
from .alexa_config import entity_supported as entity_supported_by_alexa
|
||||
@@ -41,6 +42,7 @@ from .assist_pipeline import async_create_cloud_pipeline
|
||||
from .client import CloudClient
|
||||
from .const import (
|
||||
DATA_CLOUD,
|
||||
EVENT_CLOUD_EVENT,
|
||||
LOGIN_MFA_TIMEOUT,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_DISABLE_2FA,
|
||||
@@ -278,6 +280,8 @@ class CloudLoginView(HomeAssistantView):
|
||||
new_cloud_pipeline_id = await async_create_cloud_pipeline(hass)
|
||||
else:
|
||||
new_cloud_pipeline_id = None
|
||||
|
||||
async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "login"})
|
||||
return self.json({"success": True, "cloud_pipeline": new_cloud_pipeline_id})
|
||||
|
||||
|
||||
@@ -297,6 +301,7 @@ class CloudLogoutView(HomeAssistantView):
|
||||
async with asyncio.timeout(REQUEST_TIMEOUT):
|
||||
await cloud.logout()
|
||||
|
||||
async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "logout"})
|
||||
return self.json_message("ok")
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.9.1"]
|
||||
"requirements": ["aiocomelit==0.10.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.0"]
|
||||
"requirements": ["numpy==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -46,13 +46,6 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
|
||||
hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
|
||||
|
||||
hass.http.register_view(
|
||||
SubentryManagerFlowIndexView(hass.config_entries.subentries)
|
||||
)
|
||||
hass.http.register_view(
|
||||
SubentryManagerFlowResourceView(hass.config_entries.subentries)
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, config_entries_get)
|
||||
websocket_api.async_register_command(hass, config_entry_disable)
|
||||
websocket_api.async_register_command(hass, config_entry_get_single)
|
||||
@@ -61,9 +54,6 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
websocket_api.async_register_command(hass, config_entries_progress)
|
||||
websocket_api.async_register_command(hass, ignore_config_flow)
|
||||
|
||||
websocket_api.async_register_command(hass, config_subentry_delete)
|
||||
websocket_api.async_register_command(hass, config_subentry_list)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -295,63 +285,6 @@ class OptionManagerFlowResourceView(
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
|
||||
class SubentryManagerFlowIndexView(
|
||||
FlowManagerIndexView[config_entries.ConfigSubentryFlowManager]
|
||||
):
|
||||
"""View to create subentry flows."""
|
||||
|
||||
url = "/api/config/config_entries/subentries/flow"
|
||||
name = "api:config:config_entries:subentries:flow"
|
||||
|
||||
@require_admin(
|
||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||
)
|
||||
@RequestDataValidator(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("handler"): vol.All(vol.Coerce(tuple), (str, str)),
|
||||
vol.Optional("show_advanced_options", default=False): cv.boolean,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
)
|
||||
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||
"""Handle a POST request.
|
||||
|
||||
handler in request is [entry_id, subentry_type].
|
||||
"""
|
||||
return await super()._post_impl(request, data)
|
||||
|
||||
def get_context(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return context."""
|
||||
context = super().get_context(data)
|
||||
context["source"] = config_entries.SOURCE_USER
|
||||
return context
|
||||
|
||||
|
||||
class SubentryManagerFlowResourceView(
|
||||
FlowManagerResourceView[config_entries.ConfigSubentryFlowManager]
|
||||
):
|
||||
"""View to interact with the subentry flow manager."""
|
||||
|
||||
url = "/api/config/config_entries/subentries/flow/{flow_id}"
|
||||
name = "api:config:config_entries:subentries:flow:resource"
|
||||
|
||||
@require_admin(
|
||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||
)
|
||||
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
|
||||
"""Get the current state of a data_entry_flow."""
|
||||
return await super().get(request, flow_id)
|
||||
|
||||
@require_admin(
|
||||
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
|
||||
)
|
||||
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
||||
"""Handle a POST request."""
|
||||
return await super().post(request, flow_id)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({"type": "config_entries/flow/progress"})
|
||||
def config_entries_progress(
|
||||
@@ -655,62 +588,3 @@ async def _async_matching_config_entries_json_fragments(
|
||||
)
|
||||
or (filter_is_not_helper and entry.domain not in integrations)
|
||||
]
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
"type": "config_entries/subentries/list",
|
||||
"entry_id": str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def config_subentry_list(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""List subentries of a config entry."""
|
||||
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
|
||||
if entry is None:
|
||||
return
|
||||
|
||||
result = [
|
||||
{
|
||||
"subentry_id": subentry.subentry_id,
|
||||
"title": subentry.title,
|
||||
"unique_id": subentry.unique_id,
|
||||
}
|
||||
for subentry_id, subentry in entry.subentries.items()
|
||||
]
|
||||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
"type": "config_entries/subentries/delete",
|
||||
"entry_id": str,
|
||||
"subentry_id": str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def config_subentry_delete(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Delete a subentry of a config entry."""
|
||||
entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
|
||||
if entry is None:
|
||||
return
|
||||
|
||||
try:
|
||||
hass.config_entries.async_remove_subentry(entry, msg["subentry_id"])
|
||||
except config_entries.UnknownSubEntry:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config subentry not found"
|
||||
)
|
||||
return
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.9"]
|
||||
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.20"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.TODO]
|
||||
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.TODO]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool:
|
||||
|
||||
70
homeassistant/components/cookidoo/button.py
Normal file
70
homeassistant/components/cookidoo/button.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Support for Cookidoo buttons."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from cookidoo_api import Cookidoo, CookidooException
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
|
||||
from .entity import CookidooBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CookidooButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes cookidoo button entity."""
|
||||
|
||||
press_fn: Callable[[Cookidoo], Awaitable[None]]
|
||||
|
||||
|
||||
TODO_CLEAR = CookidooButtonEntityDescription(
|
||||
key="todo_clear",
|
||||
translation_key="todo_clear",
|
||||
press_fn=lambda client: client.clear_shopping_list(),
|
||||
entity_registry_enabled_default=False,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CookidooConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Cookidoo button entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities([CookidooButton(coordinator, TODO_CLEAR)])
|
||||
|
||||
|
||||
class CookidooButton(CookidooBaseEntity, ButtonEntity):
|
||||
"""Defines an Cookidoo button."""
|
||||
|
||||
entity_description: CookidooButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CookidooDataUpdateCoordinator,
|
||||
description: CookidooButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize cookidoo button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator.cookidoo)
|
||||
except CookidooException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="button_clear_todo_failed",
|
||||
) from e
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"todo_clear": {
|
||||
"default": "mdi:cart-off"
|
||||
}
|
||||
},
|
||||
"todo": {
|
||||
"ingredient_list": {
|
||||
"default": "mdi:cart-plus"
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"todo_clear": {
|
||||
"name": "Clear shopping list and additional purchases"
|
||||
}
|
||||
},
|
||||
"todo": {
|
||||
"ingredient_list": {
|
||||
"name": "Shopping list"
|
||||
@@ -58,6 +63,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"button_clear_todo_failed": {
|
||||
"message": "Failed to clear all items from the Cookidoo shopping list"
|
||||
},
|
||||
"todo_save_item_failed": {
|
||||
"message": "Failed to save {name} to Cookidoo shopping list"
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"services": {
|
||||
"get_command": {
|
||||
"name": "Get command",
|
||||
"description": "Send sa generic HTTP get command.",
|
||||
"description": "Sends a generic HTTP get command.",
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Command",
|
||||
|
||||
@@ -81,14 +81,8 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
|
||||
or self._binary_sensor_property.sensor_type
|
||||
)
|
||||
|
||||
if device_instance.binary_sensor_property[element_uid].sub_type != "":
|
||||
self._attr_name = device_instance.binary_sensor_property[
|
||||
element_uid
|
||||
].sub_type.capitalize()
|
||||
else:
|
||||
self._attr_name = device_instance.binary_sensor_property[
|
||||
element_uid
|
||||
].sensor_type.capitalize()
|
||||
if device_instance.binary_sensor_property[element_uid].sub_type == "overload":
|
||||
self._attr_translation_key = "overload"
|
||||
|
||||
self._value = self._binary_sensor_property.state
|
||||
|
||||
@@ -129,7 +123,8 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
|
||||
|
||||
self._key = key
|
||||
self._attr_is_on = False
|
||||
self._attr_name = f"Button {key}"
|
||||
self._attr_translation_key = "button"
|
||||
self._attr_translation_placeholders = {"key": str(key)}
|
||||
|
||||
def _sync(self, message: tuple) -> None:
|
||||
"""Update the binary sensor state."""
|
||||
|
||||
@@ -116,9 +116,11 @@ class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity):
|
||||
self._multi_level_sensor_property.sensor_type
|
||||
)
|
||||
self._attr_native_unit_of_measurement = self._multi_level_sensor_property.unit
|
||||
self._attr_name = self._multi_level_sensor_property.sensor_type.capitalize()
|
||||
self._value = self._multi_level_sensor_property.value
|
||||
|
||||
if self._multi_level_sensor_property.sensor_type == "light":
|
||||
self._attr_translation_key = "brightness"
|
||||
|
||||
if element_uid.startswith("devolo.VoltageMultiLevelSensor:"):
|
||||
self._attr_entity_registry_enabled_default = False
|
||||
|
||||
@@ -128,7 +130,6 @@ class DevoloBatteryEntity(DevoloMultiLevelDeviceEntity):
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_name = "Battery level"
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
@@ -175,8 +176,6 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
|
||||
device_instance.consumption_property[element_uid], consumption
|
||||
)
|
||||
|
||||
self._attr_name = f"{consumption.capitalize()} consumption"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of the entity.
|
||||
|
||||
@@ -30,5 +30,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"button": {
|
||||
"name": "Button {key}"
|
||||
},
|
||||
"overload": {
|
||||
"name": "Overload"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"brightness": {
|
||||
"name": "Brightness"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.41.0", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.42.0", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["async-upnp-client==0.41.0"],
|
||||
"requirements": ["async-upnp-client==0.42.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
},
|
||||
"subdir": {
|
||||
"name": "Subdirectory",
|
||||
"description": "Download into subdirectory."
|
||||
"description": "Relative download path."
|
||||
},
|
||||
"filename": {
|
||||
"name": "Filename",
|
||||
"description": "Determine the filename."
|
||||
"description": "Custom name for the downloaded file."
|
||||
},
|
||||
"overwrite": {
|
||||
"name": "Overwrite",
|
||||
"description": "Whether to overwrite the file or not."
|
||||
"description": "Overwrite file if it exists."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["easyenergy==2.1.2"]
|
||||
"requirements": ["easyenergy==2.1.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@
|
||||
"services": {
|
||||
"get_gas_prices": {
|
||||
"name": "Get gas prices",
|
||||
"description": "Request gas prices from easyEnergy.",
|
||||
"description": "Requests gas prices from easyEnergy.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "Config Entry",
|
||||
"description": "The config entry to use for this service."
|
||||
"description": "The configuration entry to use for this action."
|
||||
},
|
||||
"incl_vat": {
|
||||
"name": "VAT Included",
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"get_energy_usage_prices": {
|
||||
"name": "Get energy usage prices",
|
||||
"description": "Request usage energy prices from easyEnergy.",
|
||||
"description": "Requests usage energy prices from easyEnergy.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::easyenergy::services::get_gas_prices::fields::config_entry::name%]",
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
"get_energy_return_prices": {
|
||||
"name": "Get energy return prices",
|
||||
"description": "Request return energy prices from easyEnergy.",
|
||||
"description": "Requests return energy prices from easyEnergy.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::easyenergy::services::get_gas_prices::fields::config_entry::name%]",
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from deebot_client.capabilities import CapabilityExecute, CapabilityLifeSpan
|
||||
from deebot_client.capabilities import (
|
||||
CapabilityExecute,
|
||||
CapabilityExecuteTypes,
|
||||
CapabilityLifeSpan,
|
||||
)
|
||||
from deebot_client.commands import StationAction
|
||||
from deebot_client.events import LifeSpan
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
@@ -11,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .const import SUPPORTED_LIFESPANS
|
||||
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
|
||||
from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
@@ -35,6 +40,13 @@ class EcovacsLifespanButtonEntityDescription(ButtonEntityDescription):
|
||||
component: LifeSpan
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class EcovacsStationActionButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Ecovacs station action button entity description."""
|
||||
|
||||
action: StationAction
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = (
|
||||
EcovacsButtonEntityDescription(
|
||||
capability_fn=lambda caps: caps.map.relocation if caps.map else None,
|
||||
@@ -44,6 +56,16 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsButtonEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
STATION_ENTITY_DESCRIPTIONS = tuple(
|
||||
EcovacsStationActionButtonEntityDescription(
|
||||
action=action,
|
||||
key=f"station_action_{action.name.lower()}",
|
||||
translation_key=f"station_action_{action.name.lower()}",
|
||||
)
|
||||
for action in SUPPORTED_STATION_ACTIONS
|
||||
)
|
||||
|
||||
|
||||
LIFESPAN_ENTITY_DESCRIPTIONS = tuple(
|
||||
EcovacsLifespanButtonEntityDescription(
|
||||
component=component,
|
||||
@@ -74,6 +96,15 @@ async def async_setup_entry(
|
||||
for description in LIFESPAN_ENTITY_DESCRIPTIONS
|
||||
if description.component in device.capabilities.life_span.types
|
||||
)
|
||||
entities.extend(
|
||||
EcovacsStationActionButtonEntity(
|
||||
device, device.capabilities.station.action, description
|
||||
)
|
||||
for device in controller.devices
|
||||
if device.capabilities.station
|
||||
for description in STATION_ENTITY_DESCRIPTIONS
|
||||
if description.action in device.capabilities.station.action.types
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -103,3 +134,18 @@ class EcovacsResetLifespanButtonEntity(
|
||||
await self._device.execute_command(
|
||||
self._capability.reset(self.entity_description.component)
|
||||
)
|
||||
|
||||
|
||||
class EcovacsStationActionButtonEntity(
|
||||
EcovacsDescriptionEntity[CapabilityExecuteTypes[StationAction]],
|
||||
ButtonEntity,
|
||||
):
|
||||
"""Ecovacs station action button entity."""
|
||||
|
||||
entity_description: EcovacsStationActionButtonEntityDescription
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._device.execute_command(
|
||||
self._capability.execute(self.entity_description.action)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from deebot_client.commands import StationAction
|
||||
from deebot_client.events import LifeSpan
|
||||
|
||||
DOMAIN = "ecovacs"
|
||||
@@ -19,8 +20,11 @@ SUPPORTED_LIFESPANS = (
|
||||
LifeSpan.SIDE_BRUSH,
|
||||
LifeSpan.UNIT_CARE,
|
||||
LifeSpan.ROUND_MOP,
|
||||
LifeSpan.STATION_FILTER,
|
||||
)
|
||||
|
||||
SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,)
|
||||
|
||||
LEGACY_SUPPORTED_LIFESPANS = (
|
||||
"main_brush",
|
||||
"side_brush",
|
||||
|
||||
@@ -27,11 +27,17 @@
|
||||
"reset_lifespan_side_brush": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"reset_lifespan_station_filter": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"reset_lifespan_unit_care": {
|
||||
"default": "mdi:robot-vacuum"
|
||||
},
|
||||
"reset_lifespan_round_mop": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"station_action_empty_dustbin": {
|
||||
"default": "mdi:delete-restore"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
@@ -72,6 +78,9 @@
|
||||
"lifespan_side_brush": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"lifespan_station_filter": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"lifespan_unit_care": {
|
||||
"default": "mdi:robot-vacuum"
|
||||
},
|
||||
@@ -87,6 +96,9 @@
|
||||
"network_ssid": {
|
||||
"default": "mdi:wifi"
|
||||
},
|
||||
"station_state": {
|
||||
"default": "mdi:home"
|
||||
},
|
||||
"stats_area": {
|
||||
"default": "mdi:floor-plan"
|
||||
},
|
||||
|
||||
@@ -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.10", "deebot-client==9.4.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==10.0.1"]
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class EcovacsNumberEntity(
|
||||
EcovacsDescriptionEntity[CapabilitySet[EventT, int]],
|
||||
EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]],
|
||||
NumberEntity,
|
||||
):
|
||||
"""Ecovacs number entity."""
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class EcovacsSelectEntity(
|
||||
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, str]],
|
||||
EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]],
|
||||
SelectEntity,
|
||||
):
|
||||
"""Ecovacs select entity."""
|
||||
@@ -77,7 +77,7 @@ class EcovacsSelectEntity(
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilitySetTypes[EventT, str],
|
||||
capability: CapabilitySetTypes[EventT, [str], str],
|
||||
entity_description: EcovacsSelectEntityDescription,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -16,6 +16,7 @@ from deebot_client.events import (
|
||||
NetworkInfoEvent,
|
||||
StatsEvent,
|
||||
TotalStatsEvent,
|
||||
station,
|
||||
)
|
||||
from sucks import VacBot
|
||||
|
||||
@@ -46,7 +47,7 @@ from .entity import (
|
||||
EcovacsLegacyEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_supported_entitites
|
||||
from .util import get_name_key, get_options, get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -136,6 +137,15 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
# Station
|
||||
EcovacsSensorEntityDescription[station.StationEvent](
|
||||
capability_fn=lambda caps: caps.station.state if caps.station else None,
|
||||
value_fn=lambda e: get_name_key(e.state),
|
||||
key="station_state",
|
||||
translation_key="station_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=get_options(station.State),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"relocate": {
|
||||
"name": "Relocate"
|
||||
},
|
||||
"reset_lifespan_base_station_filter": {
|
||||
"name": "Reset station filter lifespan"
|
||||
},
|
||||
"reset_lifespan_blade": {
|
||||
"name": "Reset blade lifespan"
|
||||
},
|
||||
@@ -66,6 +69,9 @@
|
||||
},
|
||||
"reset_lifespan_side_brush": {
|
||||
"name": "Reset side brush lifespan"
|
||||
},
|
||||
"station_action_empty_dustbin": {
|
||||
"name": "Empty dustbin"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
@@ -107,6 +113,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lifespan_base_station_filter": {
|
||||
"name": "Station filter lifespan"
|
||||
},
|
||||
"lifespan_blade": {
|
||||
"name": "Blade lifespan"
|
||||
},
|
||||
@@ -140,6 +149,13 @@
|
||||
"network_ssid": {
|
||||
"name": "Wi-Fi SSID"
|
||||
},
|
||||
"station_state": {
|
||||
"name": "Station state",
|
||||
"state": {
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"emptying_dustbin": "Emptying dustbin"
|
||||
}
|
||||
},
|
||||
"stats_area": {
|
||||
"name": "Area cleaned"
|
||||
},
|
||||
|
||||
@@ -131,7 +131,7 @@ class EcovacsSwitchEntity(
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def on_event(event: EnableEvent) -> None:
|
||||
self._attr_is_on = event.enable
|
||||
self._attr_is_on = event.enabled
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscribe(self._capability.event, on_event)
|
||||
|
||||
@@ -7,6 +7,8 @@ import random
|
||||
import string
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deebot_client.events.station import State
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
@@ -47,4 +49,13 @@ def get_supported_entitites(
|
||||
@callback
|
||||
def get_name_key(enum: Enum) -> str:
|
||||
"""Return the lower case name of the enum."""
|
||||
if enum is State.EMPTYING:
|
||||
# Will be fixed in the next major release of deebot-client
|
||||
return "emptying_dustbin"
|
||||
return enum.name.lower()
|
||||
|
||||
|
||||
@callback
|
||||
def get_options(enum: type[Enum]) -> list[str]:
|
||||
"""Return the options for the enum."""
|
||||
return [get_name_key(option) for option in enum]
|
||||
|
||||
@@ -6,11 +6,16 @@ from dataclasses import dataclass
|
||||
|
||||
from elevenlabs import AsyncElevenLabs, Model
|
||||
from elevenlabs.core import ApiError
|
||||
from httpx import ConnectError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_MODEL
|
||||
@@ -35,10 +40,10 @@ class ElevenLabsData:
|
||||
model: Model
|
||||
|
||||
|
||||
type EleventLabsConfigEntry = ConfigEntry[ElevenLabsData]
|
||||
type ElevenLabsConfigEntry = ConfigEntry[ElevenLabsData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: EleventLabsConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ElevenLabsConfigEntry) -> bool:
|
||||
"""Set up ElevenLabs text-to-speech from a config entry."""
|
||||
entry.add_update_listener(update_listener)
|
||||
httpx_client = get_async_client(hass)
|
||||
@@ -48,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: EleventLabsConfigEntry)
|
||||
model_id = entry.options[CONF_MODEL]
|
||||
try:
|
||||
model = await get_model_by_id(client, model_id)
|
||||
except ConnectError as err:
|
||||
raise ConfigEntryNotReady("Failed to connect") from err
|
||||
except ApiError as err:
|
||||
raise ConfigEntryAuthFailed("Auth failed") from err
|
||||
|
||||
@@ -60,15 +67,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: EleventLabsConfigEntry)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: EleventLabsConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElevenLabsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(
|
||||
hass: HomeAssistant, config_entry: EleventLabsConfigEntry
|
||||
hass: HomeAssistant, config_entry: ElevenLabsConfigEntry
|
||||
) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
)
|
||||
|
||||
from . import EleventLabsConfigEntry
|
||||
from . import ElevenLabsConfigEntry
|
||||
from .const import (
|
||||
CONF_CONFIGURE_VOICE,
|
||||
CONF_MODEL,
|
||||
@@ -92,7 +92,7 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
def async_get_options_flow(
|
||||
config_entry: EleventLabsConfigEntry,
|
||||
config_entry: ElevenLabsConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Create the options flow."""
|
||||
return ElevenLabsOptionsFlow(config_entry)
|
||||
@@ -101,7 +101,7 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class ElevenLabsOptionsFlow(OptionsFlow):
|
||||
"""ElevenLabs options flow."""
|
||||
|
||||
def __init__(self, config_entry: EleventLabsConfigEntry) -> None:
|
||||
def __init__(self, config_entry: ElevenLabsConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.api_key: str = config_entry.data[CONF_API_KEY]
|
||||
# id -> name
|
||||
|
||||
@@ -7,17 +7,13 @@ rules:
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: >
|
||||
We should have every test end in either ABORT or CREATE_ENTRY.
|
||||
test_invalid_api_key should assert the kind of error that is raised.
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: >
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import EleventLabsConfigEntry
|
||||
from . import ElevenLabsConfigEntry
|
||||
from .const import (
|
||||
CONF_OPTIMIZE_LATENCY,
|
||||
CONF_SIMILARITY,
|
||||
@@ -56,7 +56,7 @@ def to_voice_settings(options: MappingProxyType[str, Any]) -> VoiceSettings:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: EleventLabsConfigEntry,
|
||||
config_entry: ElevenLabsConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up ElevenLabs tts platform via config entry."""
|
||||
|
||||
@@ -49,7 +49,7 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
|
||||
_element: Zone
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
# Zone in NORMAL state is OFF; any other state is ON
|
||||
self._attr_is_on = bool(
|
||||
self._element.logical_status != ZoneLogicalStatus.NORMAL
|
||||
|
||||
@@ -120,7 +120,7 @@ class ElkCounter(ElkSensor):
|
||||
_attr_icon = "mdi:numeric"
|
||||
_element: Counter
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
self._attr_native_value = self._element.value
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class ElkKeypad(ElkSensor):
|
||||
attrs["last_keypress"] = self._element.last_keypress
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
self._attr_native_value = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPERATURE
|
||||
)
|
||||
@@ -173,7 +173,7 @@ class ElkPanel(ElkSensor):
|
||||
attrs["system_trouble_status"] = self._element.system_trouble_status
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._elk.is_connected():
|
||||
self._attr_native_value = (
|
||||
"Paused" if self._element.remote_programming_status else "Connected"
|
||||
@@ -188,7 +188,7 @@ class ElkSetting(ElkSensor):
|
||||
_attr_translation_key = "setting"
|
||||
_element: Setting
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
self._attr_native_value = self._element.value
|
||||
|
||||
@property
|
||||
@@ -257,7 +257,7 @@ class ElkZone(ElkSensor):
|
||||
return UnitOfElectricPotential.VOLT
|
||||
return None
|
||||
|
||||
def _element_changed(self, _: Element, changeset: Any) -> None:
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
self._attr_native_value = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPERATURE
|
||||
|
||||
@@ -151,7 +151,9 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
port=self._panel_direct_port,
|
||||
)
|
||||
)
|
||||
ssl_context = build_direct_ssl_context(cadata=self._panel_direct_ssl_cert)
|
||||
ssl_context = await self.hass.async_add_executor_job(
|
||||
build_direct_ssl_context, self._panel_direct_ssl_cert
|
||||
)
|
||||
|
||||
# Attempt the connection to make sure the pin works. Also, take the chance to retrieve the panel ID via APIs.
|
||||
client_api_url = get_direct_api_url(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/elmax",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["elmax_api"],
|
||||
"requirements": ["elmax-api==0.0.6.3"],
|
||||
"requirements": ["elmax-api==0.0.6.4rc0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_elmax-ssl._tcp.local."
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"panel_pin": "Panel Pin"
|
||||
"panel_pin": "Panel PIN"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -58,7 +58,7 @@
|
||||
"no_panel_online": "No online Elmax control panel was found.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"network_error": "A network error occurred",
|
||||
"invalid_pin": "The provided pin is invalid",
|
||||
"invalid_pin": "The provided PIN is invalid",
|
||||
"invalid_mode": "Invalid or unsupported mode",
|
||||
"reauth_panel_disappeared": "The given panel is no longer associated to this user. Please log in using an account associated to this panel.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
|
||||
@@ -51,8 +51,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
# wait for the next discovery to find the device at its new address
|
||||
# and update the config entry so we do not mix up devices.
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unexpected device found at {host}; expected {entry.unique_id}, "
|
||||
f"found {envoy.serial_number}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unexpected_device",
|
||||
translation_placeholders={
|
||||
"host": host,
|
||||
"expected_serial": str(entry.unique_id),
|
||||
"actual_serial": str(envoy.serial_number),
|
||||
},
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
@@ -72,7 +77,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
coordinator: EnphaseUpdateCoordinator = entry.runtime_data
|
||||
coordinator = entry.runtime_data
|
||||
coordinator.async_cancel_token_refresh()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -141,9 +141,13 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
and entry.data[CONF_HOST] == self.ip_address
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Zeroconf update envoy with this ip and blank serial in unique_id",
|
||||
"Zeroconf update envoy with this ip and blank unique_id",
|
||||
)
|
||||
title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY
|
||||
# Found an entry with blank unique_id (prior deleted) with same ip
|
||||
# If the title is still default shorthand 'Envoy' then append serial
|
||||
# to differentiate multiple Envoy. Don't change the title if any other
|
||||
# title is still present in the old entry.
|
||||
title = f"{ENVOY} {serial}" if entry.title == ENVOY else entry.title
|
||||
return self.async_update_reload_and_abort(
|
||||
entry, title=title, unique_id=serial, reason="already_configured"
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import INVALID_AUTH_ERRORS
|
||||
from .const import DOMAIN, INVALID_AUTH_ERRORS
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
@@ -37,6 +37,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
envoy_serial_number: str
|
||||
envoy_firmware: str
|
||||
config_entry: EnphaseConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, envoy: Envoy, entry: EnphaseConfigEntry
|
||||
@@ -44,7 +45,6 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Initialize DataUpdateCoordinator for the envoy."""
|
||||
self.envoy = envoy
|
||||
entry_data = entry.data
|
||||
self.entry = entry
|
||||
self.username = entry_data[CONF_USERNAME]
|
||||
self.password = entry_data[CONF_PASSWORD]
|
||||
self._setup_complete = False
|
||||
@@ -107,7 +107,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
await envoy.setup()
|
||||
assert envoy.serial_number is not None
|
||||
self.envoy_serial_number = envoy.serial_number
|
||||
if token := self.entry.data.get(CONF_TOKEN):
|
||||
if token := self.config_entry.data.get(CONF_TOKEN):
|
||||
with contextlib.suppress(*INVALID_AUTH_ERRORS):
|
||||
# Always set the username and password
|
||||
# so we can refresh the token if needed
|
||||
@@ -136,9 +136,9 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# as long as the token is valid
|
||||
_LOGGER.debug("%s: Updating token in config entry from auth", self.name)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
self.config_entry,
|
||||
data={
|
||||
**self.entry.data,
|
||||
**self.config_entry.data,
|
||||
CONF_TOKEN: envoy.auth.token,
|
||||
},
|
||||
)
|
||||
@@ -158,9 +158,23 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
# token likely expired or firmware changed, try to re-authenticate
|
||||
self._setup_complete = False
|
||||
continue
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
translation_placeholders={
|
||||
"host": envoy.host,
|
||||
"args": err.args[0],
|
||||
},
|
||||
) from err
|
||||
except EnvoyError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="envoy_error",
|
||||
translation_placeholders={
|
||||
"host": envoy.host,
|
||||
"args": err.args[0],
|
||||
},
|
||||
) from err
|
||||
|
||||
# if we have a firmware version from previous setup, compare to current one
|
||||
# when envoy gets new firmware there will be an authentication failure
|
||||
@@ -175,7 +189,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
# reload the integration to get all established again
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.entry.entry_id)
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
# remember firmware version for next time
|
||||
self.envoy_firmware = envoy.firmware
|
||||
|
||||
@@ -7,29 +7,16 @@ rules:
|
||||
status: done
|
||||
comment: fixed 1 minute cycle based on Enphase Envoy device characteristics
|
||||
brands: done
|
||||
common-modules:
|
||||
status: done
|
||||
comment: |
|
||||
In coordinator.py, you set self.entry = entry, while after the super constructor,
|
||||
you can access the entry via self.config_entry (you would have to overwrite the
|
||||
type to make sure you don't have to assert not None every time)done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
- test_form is missing an assertion for the unique id of the resulting entry
|
||||
- Let's also have test_user_no_serial_number assert the unique_id (as in, it can't be set to the serial_number since we dont have one, so let's assert what it will result in)
|
||||
- Let's have every test result in either CREATE_ENTRY or ABORT (like test_form_invalid_auth or test_form_cannot_connect, they can be parametrized)
|
||||
- test_zeroconf_token_firmware and test_zeroconf_pre_token_firmware can also be parametrized I think
|
||||
- test_zero_conf_malformed_serial_property - with pytest.raises(KeyError) as ex::
|
||||
I don't believe this should be able to raise a KeyError Shouldn't we abort the flow?
|
||||
test_reauth -> Let's also assert result before we start with the async_configure part
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
- async_step_zeroconf -> a config entry title is considered userland,
|
||||
so if someone renamed their entry, it will be reverted back with the code at L146.
|
||||
- async_step_reaut L160: I believe that the unique is already set when starting a reauth flow
|
||||
- The config flow is missing data descriptions for the other fields
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: done
|
||||
@@ -48,11 +35,7 @@ rules:
|
||||
comment: no events used.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data:
|
||||
status: done
|
||||
comment: |
|
||||
async_unload_entry- coordinator: EnphaseUpdateCoordinator = entry.runtime_data
|
||||
You can remove the EnphaseUpdateCoordinator as the type can now be inferred thanks to the typed config entry
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
@@ -108,9 +91,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: pending https://github.com/home-assistant/core/pull/132483
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
|
||||
@@ -10,6 +10,8 @@ from operator import attrgetter
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyenphase import (
|
||||
EnvoyACBPower,
|
||||
EnvoyBatteryAggregate,
|
||||
EnvoyEncharge,
|
||||
EnvoyEnchargeAggregate,
|
||||
EnvoyEnchargePower,
|
||||
@@ -723,6 +725,78 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EnvoyAcbBatterySensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Envoy ACB Battery sensor entity."""
|
||||
|
||||
value_fn: Callable[[EnvoyACBPower], int | str]
|
||||
|
||||
|
||||
ACB_BATTERY_POWER_SENSORS = (
|
||||
EnvoyAcbBatterySensorEntityDescription(
|
||||
key="acb_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
value_fn=attrgetter("power"),
|
||||
),
|
||||
EnvoyAcbBatterySensorEntityDescription(
|
||||
key="acb_soc",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
value_fn=attrgetter("state_of_charge"),
|
||||
),
|
||||
EnvoyAcbBatterySensorEntityDescription(
|
||||
key="acb_battery_state",
|
||||
translation_key="acb_battery_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["discharging", "idle", "charging", "full"],
|
||||
value_fn=attrgetter("state"),
|
||||
),
|
||||
)
|
||||
|
||||
ACB_BATTERY_ENERGY_SENSORS = (
|
||||
EnvoyAcbBatterySensorEntityDescription(
|
||||
key="acb_available_energy",
|
||||
translation_key="acb_available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("charge_wh"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EnvoyAggregateBatterySensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Envoy aggregate Ensemble and ACB Battery sensor entity."""
|
||||
|
||||
value_fn: Callable[[EnvoyBatteryAggregate], int]
|
||||
|
||||
|
||||
AGGREGATE_BATTERY_SENSORS = (
|
||||
EnvoyAggregateBatterySensorEntityDescription(
|
||||
key="aggregated_soc",
|
||||
translation_key="aggregated_soc",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
value_fn=attrgetter("state_of_charge"),
|
||||
),
|
||||
EnvoyAggregateBatterySensorEntityDescription(
|
||||
key="aggregated_available_energy",
|
||||
translation_key="aggregated_available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("available_energy"),
|
||||
),
|
||||
EnvoyAggregateBatterySensorEntityDescription(
|
||||
key="aggregated_max_battery_capacity",
|
||||
translation_key="aggregated_max_capacity",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("max_available_capacity"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: EnphaseConfigEntry,
|
||||
@@ -847,6 +921,20 @@ async def async_setup_entry(
|
||||
EnvoyEnpowerEntity(coordinator, description)
|
||||
for description in ENPOWER_SENSORS
|
||||
)
|
||||
if envoy_data.acb_power:
|
||||
entities.extend(
|
||||
EnvoyAcbBatteryPowerEntity(coordinator, description)
|
||||
for description in ACB_BATTERY_POWER_SENSORS
|
||||
)
|
||||
entities.extend(
|
||||
EnvoyAcbBatteryEnergyEntity(coordinator, description)
|
||||
for description in ACB_BATTERY_ENERGY_SENSORS
|
||||
)
|
||||
if envoy_data.battery_aggregate:
|
||||
entities.extend(
|
||||
AggregateBatteryEntity(coordinator, description)
|
||||
for description in AGGREGATE_BATTERY_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -1228,3 +1316,60 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
|
||||
enpower = self.data.enpower
|
||||
assert enpower is not None
|
||||
return self.entity_description.value_fn(enpower)
|
||||
|
||||
|
||||
class EnvoyAcbBatteryPowerEntity(EnvoySensorBaseEntity):
|
||||
"""Envoy ACB Battery power sensor entity."""
|
||||
|
||||
entity_description: EnvoyAcbBatterySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EnphaseUpdateCoordinator,
|
||||
description: EnvoyAcbBatterySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize ACB Battery entity."""
|
||||
super().__init__(coordinator, description)
|
||||
acb_data = self.data.acb_power
|
||||
assert acb_data is not None
|
||||
self._attr_unique_id = f"{self.envoy_serial_num}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{self.envoy_serial_num}_acb")},
|
||||
manufacturer="Enphase",
|
||||
model="ACB",
|
||||
name=f"ACB {self.envoy_serial_num}",
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | str | None:
|
||||
"""Return the state of the ACB Battery power sensors."""
|
||||
acb = self.data.acb_power
|
||||
assert acb is not None
|
||||
return self.entity_description.value_fn(acb)
|
||||
|
||||
|
||||
class EnvoyAcbBatteryEnergyEntity(EnvoySystemSensorEntity):
|
||||
"""Envoy combined ACB and Ensemble Battery Aggregate energy sensor entity."""
|
||||
|
||||
entity_description: EnvoyAcbBatterySensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | str:
|
||||
"""Return the state of the aggregate energy sensors."""
|
||||
acb = self.data.acb_power
|
||||
assert acb is not None
|
||||
return self.entity_description.value_fn(acb)
|
||||
|
||||
|
||||
class AggregateBatteryEntity(EnvoySystemSensorEntity):
|
||||
"""Envoy combined ACB and Ensemble Battery Aggregate sensor entity."""
|
||||
|
||||
entity_description: EnvoyAggregateBatterySensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the state of the aggregate sensors."""
|
||||
battery_aggregate = self.data.battery_aggregate
|
||||
assert battery_aggregate is not None
|
||||
return self.entity_description.value_fn(battery_aggregate)
|
||||
|
||||
@@ -337,6 +337,30 @@
|
||||
},
|
||||
"configured_reserve_soc": {
|
||||
"name": "Configured reserve battery level"
|
||||
},
|
||||
"acb_battery_state": {
|
||||
"name": "Battery state",
|
||||
"state": {
|
||||
"discharging": "Discharging",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"charging": "Charging",
|
||||
"full": "Full"
|
||||
}
|
||||
},
|
||||
"acb_available_energy": {
|
||||
"name": "Available ACB battery energy"
|
||||
},
|
||||
"acb_max_capacity": {
|
||||
"name": "ACB Battery capacity"
|
||||
},
|
||||
"aggregated_available_energy": {
|
||||
"name": "Aggregated available battery energy"
|
||||
},
|
||||
"aggregated_max_capacity": {
|
||||
"name": "Aggregated Battery capacity"
|
||||
},
|
||||
"aggregated_soc": {
|
||||
"name": "Aggregated battery soc"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
@@ -347,5 +371,16 @@
|
||||
"name": "Grid enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"unexpected_device": {
|
||||
"message": "Unexpected Envoy serial-number found at {host}; expected {expected_serial}, found {actual_serial}"
|
||||
},
|
||||
"authentication_error": {
|
||||
"message": "Envoy authentication failure on {host}: {args}"
|
||||
},
|
||||
"envoy_error": {
|
||||
"message": "Error communicating with Envoy API on {host}: {args}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,8 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
@esphome_float_state_property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if not self._static_info.supports_current_temperature:
|
||||
return None
|
||||
return self._state.current_temperature
|
||||
|
||||
@property
|
||||
|
||||
@@ -14,6 +14,7 @@ import feedparser
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -101,7 +102,11 @@ class FeedReaderCoordinator(
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the feed manager."""
|
||||
feed = await self._async_fetch_feed()
|
||||
try:
|
||||
feed = await self._async_fetch_feed()
|
||||
except UpdateFailed as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
self.logger.debug("Feed data fetched from %s : %s", self.url, feed["feed"])
|
||||
if feed_author := feed["feed"].get("author"):
|
||||
self.feed_author = html.unescape(feed_author)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"title": "Filter",
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "[%key:common::action::reload%]",
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fjaraskupan import COMMAND_LIGHT_ON_OFF
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -62,7 +60,6 @@ class Light(CoordinatorEntity[FjaraskupanCoordinator], LightEntity):
|
||||
if self.is_on:
|
||||
async with self.coordinator.async_connect_and_update() as device:
|
||||
await device.send_dim(0)
|
||||
await device.send_command(COMMAND_LIGHT_ON_OFF)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/fjaraskupan",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "fjaraskupan"],
|
||||
"requirements": ["fjaraskupan==2.3.0"]
|
||||
"requirements": ["fjaraskupan==2.3.2"]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ from . import FlexitCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import FlexitEntity
|
||||
|
||||
_MAX_FAN_SETPOINT = 100
|
||||
_MIN_FAN_SETPOINT = 30
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class FlexitNumberEntityDescription(NumberEntityDescription):
|
||||
@@ -34,6 +37,24 @@ class FlexitNumberEntityDescription(NumberEntityDescription):
|
||||
set_native_value_fn: Callable[[FlexitBACnet], Callable[[int], Awaitable[None]]]
|
||||
|
||||
|
||||
# Setpoints for Away, Home and High are dependent of each other. Fireplace and Cooker Hood
|
||||
# have setpoints between 0 (MIN_FAN_SETPOINT) and 100 (MAX_FAN_SETPOINT).
|
||||
# See the table below for all the setpoints.
|
||||
#
|
||||
# | Mode | Setpoint | Min | Max |
|
||||
# |:------------|----------|:----------------------|:----------------------|
|
||||
# | HOME | Supply | AWAY Supply setpoint | 100 |
|
||||
# | HOME | Extract | AWAY Extract setpoint | 100 |
|
||||
# | AWAY | Supply | 30 | HOME Supply setpoint |
|
||||
# | AWAY | Extract | 30 | HOME Extract setpoint |
|
||||
# | HIGH | Supply | HOME Supply setpoint | 100 |
|
||||
# | HIGH | Extract | HOME Extract setpoint | 100 |
|
||||
# | COOKER_HOOD | Supply | 30 | 100 |
|
||||
# | COOKER_HOOD | Extract | 30 | 100 |
|
||||
# | FIREPLACE | Supply | 30 | 100 |
|
||||
# | FIREPLACE | Extract | 30 | 100 |
|
||||
|
||||
|
||||
NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
FlexitNumberEntityDescription(
|
||||
key="away_extract_fan_setpoint",
|
||||
@@ -45,7 +66,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_away,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda device: int(device.fan_setpoint_extract_air_home),
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="away_supply_fan_setpoint",
|
||||
@@ -57,7 +78,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_away,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda device: int(device.fan_setpoint_supply_air_home),
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="cooker_hood_extract_fan_setpoint",
|
||||
@@ -68,8 +89,8 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_extract_air_cooker,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_cooker,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="cooker_hood_supply_fan_setpoint",
|
||||
@@ -80,8 +101,8 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_supply_air_cooker,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_cooker,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="fireplace_extract_fan_setpoint",
|
||||
@@ -92,8 +113,8 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_extract_air_fire,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_fire,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="fireplace_supply_fan_setpoint",
|
||||
@@ -104,8 +125,8 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_supply_air_fire,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_fire,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_min_value_fn=lambda _: 30,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda _: _MIN_FAN_SETPOINT,
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
key="high_extract_fan_setpoint",
|
||||
@@ -116,7 +137,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_extract_air_high,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_high,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_home),
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
@@ -128,7 +149,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_supply_air_high,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_high,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_home),
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
@@ -140,7 +161,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_extract_air_home,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_home,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda device: int(device.fan_setpoint_extract_air_away),
|
||||
),
|
||||
FlexitNumberEntityDescription(
|
||||
@@ -152,7 +173,7 @@ NUMBERS: tuple[FlexitNumberEntityDescription, ...] = (
|
||||
native_value_fn=lambda device: device.fan_setpoint_supply_air_home,
|
||||
set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_home,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
native_max_value_fn=lambda _: 100,
|
||||
native_max_value_fn=lambda _: _MAX_FAN_SETPOINT,
|
||||
native_min_value_fn=lambda device: int(device.fan_setpoint_supply_air_away),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["flux_led"],
|
||||
"requirements": ["flux-led==1.0.4"]
|
||||
"requirements": ["flux-led==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["freebox_api"],
|
||||
"requirements": ["freebox-api==1.1.0"],
|
||||
"requirements": ["freebox-api==1.2.1"],
|
||||
"zeroconf": ["_fbx-api._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class FritzboxCoordinatorData:
|
||||
|
||||
devices: dict[str, FritzhomeDevice]
|
||||
templates: dict[str, FritzhomeTemplate]
|
||||
supported_color_properties: dict[str, tuple[dict, list]]
|
||||
|
||||
|
||||
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
|
||||
@@ -49,7 +50,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
self.new_devices: set[str] = set()
|
||||
self.new_templates: set[str] = set()
|
||||
|
||||
self.data = FritzboxCoordinatorData({}, {})
|
||||
self.data = FritzboxCoordinatorData({}, {}, {})
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
@@ -120,6 +121,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
|
||||
devices = self.fritz.get_devices()
|
||||
device_data = {}
|
||||
supported_color_properties = self.data.supported_color_properties
|
||||
for device in devices:
|
||||
# assume device as unavailable, see #55799
|
||||
if (
|
||||
@@ -136,6 +138,13 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
|
||||
device_data[device.ain] = device
|
||||
|
||||
# pre-load supported colors and color temps for new devices
|
||||
if device.has_color and device.ain not in supported_color_properties:
|
||||
supported_color_properties[device.ain] = (
|
||||
device.get_colors(),
|
||||
device.get_color_temps(),
|
||||
)
|
||||
|
||||
template_data = {}
|
||||
if self.has_templates:
|
||||
templates = self.fritz.get_templates()
|
||||
@@ -145,7 +154,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
self.new_devices = device_data.keys() - self.data.devices.keys()
|
||||
self.new_templates = template_data.keys() - self.data.templates.keys()
|
||||
|
||||
return FritzboxCoordinatorData(devices=device_data, templates=template_data)
|
||||
return FritzboxCoordinatorData(
|
||||
devices=device_data,
|
||||
templates=template_data,
|
||||
supported_color_properties=supported_color_properties,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> FritzboxCoordinatorData:
|
||||
"""Fetch all device data."""
|
||||
|
||||
@@ -57,7 +57,6 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
) -> None:
|
||||
"""Initialize the FritzboxLight entity."""
|
||||
super().__init__(coordinator, ain, None)
|
||||
self._supported_hs: dict[int, list[int]] = {}
|
||||
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
if self.data.has_color:
|
||||
@@ -65,6 +64,26 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
elif self.data.has_level:
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
(supported_colors, supported_color_temps) = (
|
||||
coordinator.data.supported_color_properties.get(self.data.ain, ({}, []))
|
||||
)
|
||||
|
||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||
self._supported_hs: dict[int, list[int]] = {}
|
||||
for values in supported_colors.values():
|
||||
hue = int(values[0][0])
|
||||
self._supported_hs[hue] = [
|
||||
int(values[0][1]),
|
||||
int(values[1][1]),
|
||||
int(values[2][1]),
|
||||
]
|
||||
|
||||
if supported_color_temps:
|
||||
# only available for color bulbs
|
||||
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
||||
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""If the light is currently on or off."""
|
||||
@@ -148,30 +167,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
"""Turn the light off."""
|
||||
await self.hass.async_add_executor_job(self.data.set_state_off)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Get light attributes from device after entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
def _get_color_data() -> tuple[dict, list]:
|
||||
return (self.data.get_colors(), self.data.get_color_temps())
|
||||
|
||||
(
|
||||
supported_colors,
|
||||
supported_color_temps,
|
||||
) = await self.hass.async_add_executor_job(_get_color_data)
|
||||
|
||||
if supported_color_temps:
|
||||
# only available for color bulbs
|
||||
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
||||
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
||||
|
||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||
for values in supported_colors.values():
|
||||
hue = int(values[0][0])
|
||||
self._supported_hs[hue] = [
|
||||
int(values[0][1]),
|
||||
int(values[1][1]),
|
||||
int(values[2][1]),
|
||||
]
|
||||
|
||||
@@ -68,6 +68,167 @@ def get_inverter_status_message(code: StateType) -> InverterStatusCodeOption | N
|
||||
return _INVERTER_STATUS_CODES.get(code) # type: ignore[arg-type]
|
||||
|
||||
|
||||
INVERTER_ERROR_CODES: Final[dict[int, str]] = {
|
||||
0: "no_error",
|
||||
102: "ac_voltage_too_high",
|
||||
103: "ac_voltage_too_low",
|
||||
105: "ac_frequency_too_high",
|
||||
106: "ac_frequency_too_low",
|
||||
107: "ac_grid_outside_permissible_limits",
|
||||
108: "stand_alone_operation_detected",
|
||||
112: "rcmu_error",
|
||||
240: "arc_detection_triggered",
|
||||
241: "arc_detection_triggered",
|
||||
242: "arc_detection_triggered",
|
||||
243: "arc_detection_triggered",
|
||||
301: "overcurrent_ac",
|
||||
302: "overcurrent_dc",
|
||||
303: "dc_module_over_temperature",
|
||||
304: "ac_module_over_temperature",
|
||||
305: "no_power_fed_in_despite_closed_relay",
|
||||
306: "pv_output_too_low_for_feeding_energy_into_the_grid",
|
||||
307: "low_pv_voltage_dc_input_voltage_too_low",
|
||||
308: "intermediate_circuit_voltage_too_high",
|
||||
309: "dc_input_voltage_mppt_1_too_high",
|
||||
311: "polarity_of_dc_strings_reversed",
|
||||
313: "dc_input_voltage_mppt_2_too_high",
|
||||
314: "current_sensor_calibration_timeout",
|
||||
315: "ac_current_sensor_error",
|
||||
316: "interrupt_check_fail",
|
||||
325: "overtemperature_in_connection_area",
|
||||
326: "fan_1_error",
|
||||
327: "fan_2_error",
|
||||
401: "no_communication_with_power_stage_set",
|
||||
406: "ac_module_temperature_sensor_faulty_l1",
|
||||
407: "ac_module_temperature_sensor_faulty_l2",
|
||||
408: "dc_component_measured_in_grid_too_high",
|
||||
412: "fixed_voltage_mode_out_of_range",
|
||||
415: "safety_cut_out_triggered",
|
||||
416: "no_communication_between_power_stage_and_control_system",
|
||||
417: "hardware_id_problem",
|
||||
419: "unique_id_conflict",
|
||||
420: "no_communication_with_hybrid_manager",
|
||||
421: "hid_range_error",
|
||||
425: "no_communication_with_power_stage_set",
|
||||
426: "possible_hardware_fault",
|
||||
427: "possible_hardware_fault",
|
||||
428: "possible_hardware_fault",
|
||||
431: "software_problem",
|
||||
436: "functional_incompatibility_between_pc_boards",
|
||||
437: "power_stage_set_problem",
|
||||
438: "functional_incompatibility_between_pc_boards",
|
||||
443: "intermediate_circuit_voltage_too_low_or_asymmetric",
|
||||
445: "compatibility_error_invalid_power_stage_configuration",
|
||||
447: "insulation_fault",
|
||||
448: "neutral_conductor_not_connected",
|
||||
450: "guard_cannot_be_found",
|
||||
451: "memory_error_detected",
|
||||
452: "communication",
|
||||
502: "insulation_error_on_solar_panels",
|
||||
509: "no_energy_fed_into_grid_past_24_hours",
|
||||
515: "no_communication_with_filter",
|
||||
516: "no_communication_with_storage_unit",
|
||||
517: "power_derating_due_to_high_temperature",
|
||||
518: "internal_dsp_malfunction",
|
||||
519: "no_communication_with_storage_unit",
|
||||
520: "no_energy_fed_by_mppt1_past_24_hours",
|
||||
522: "dc_low_string_1",
|
||||
523: "dc_low_string_2",
|
||||
558: "functional_incompatibility_between_pc_boards",
|
||||
559: "functional_incompatibility_between_pc_boards",
|
||||
560: "derating_caused_by_over_frequency",
|
||||
564: "functional_incompatibility_between_pc_boards",
|
||||
566: "arc_detector_switched_off",
|
||||
567: "grid_voltage_dependent_power_reduction_active",
|
||||
601: "can_bus_full",
|
||||
603: "ac_module_temperature_sensor_faulty_l3",
|
||||
604: "dc_module_temperature_sensor_faulty",
|
||||
607: "rcmu_error",
|
||||
608: "functional_incompatibility_between_pc_boards",
|
||||
701: "internal_processor_status",
|
||||
702: "internal_processor_status",
|
||||
703: "internal_processor_status",
|
||||
704: "internal_processor_status",
|
||||
705: "internal_processor_status",
|
||||
706: "internal_processor_status",
|
||||
707: "internal_processor_status",
|
||||
708: "internal_processor_status",
|
||||
709: "internal_processor_status",
|
||||
710: "internal_processor_status",
|
||||
711: "internal_processor_status",
|
||||
712: "internal_processor_status",
|
||||
713: "internal_processor_status",
|
||||
714: "internal_processor_status",
|
||||
715: "internal_processor_status",
|
||||
716: "internal_processor_status",
|
||||
721: "eeprom_reinitialised",
|
||||
722: "internal_processor_status",
|
||||
723: "internal_processor_status",
|
||||
724: "internal_processor_status",
|
||||
725: "internal_processor_status",
|
||||
726: "internal_processor_status",
|
||||
727: "internal_processor_status",
|
||||
728: "internal_processor_status",
|
||||
729: "internal_processor_status",
|
||||
730: "internal_processor_status",
|
||||
731: "initialisation_error_usb_flash_drive_not_supported",
|
||||
732: "initialisation_error_usb_stick_over_current",
|
||||
733: "no_usb_flash_drive_connected",
|
||||
734: "update_file_not_recognised_or_missing",
|
||||
735: "update_file_does_not_match_device",
|
||||
736: "write_or_read_error_occurred",
|
||||
737: "file_could_not_be_opened",
|
||||
738: "log_file_cannot_be_saved",
|
||||
740: "initialisation_error_file_system_error_on_usb",
|
||||
741: "error_during_logging_data_recording",
|
||||
743: "error_during_update_process",
|
||||
745: "update_file_corrupt",
|
||||
746: "error_during_update_process",
|
||||
751: "time_lost",
|
||||
752: "real_time_clock_communication_error",
|
||||
753: "real_time_clock_in_emergency_mode",
|
||||
754: "internal_processor_status",
|
||||
755: "internal_processor_status",
|
||||
757: "real_time_clock_hardware_error",
|
||||
758: "real_time_clock_in_emergency_mode",
|
||||
760: "internal_hardware_error",
|
||||
761: "internal_processor_status",
|
||||
762: "internal_processor_status",
|
||||
763: "internal_processor_status",
|
||||
764: "internal_processor_status",
|
||||
765: "internal_processor_status",
|
||||
766: "emergency_power_derating_activated",
|
||||
767: "internal_processor_status",
|
||||
768: "different_power_limitation_in_hardware_modules",
|
||||
772: "storage_unit_not_available",
|
||||
773: "software_update_invalid_country_setup",
|
||||
775: "pmc_power_stage_set_not_available",
|
||||
776: "invalid_device_type",
|
||||
781: "internal_processor_status",
|
||||
782: "internal_processor_status",
|
||||
783: "internal_processor_status",
|
||||
784: "internal_processor_status",
|
||||
785: "internal_processor_status",
|
||||
786: "internal_processor_status",
|
||||
787: "internal_processor_status",
|
||||
788: "internal_processor_status",
|
||||
789: "internal_processor_status",
|
||||
790: "internal_processor_status",
|
||||
791: "internal_processor_status",
|
||||
792: "internal_processor_status",
|
||||
793: "internal_processor_status",
|
||||
794: "internal_processor_status",
|
||||
1001: "insulation_measurement_triggered",
|
||||
1024: "inverter_settings_changed_restart_required",
|
||||
1030: "wired_shut_down_triggered",
|
||||
1036: "grid_frequency_exceeded_limit_reconnecting",
|
||||
1112: "mains_voltage_dependent_power_reduction",
|
||||
1175: "too_little_dc_power_for_feed_in_operation",
|
||||
1196: "inverter_required_setup_values_not_received",
|
||||
65000: "dc_connection_inverter_battery_interrupted",
|
||||
}
|
||||
|
||||
|
||||
class MeterLocationCodeOption(StrEnum):
|
||||
"""Meter location codes for Fronius meters."""
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/fronius",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfronius"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PyFronius==0.7.3"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user