mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 16:39:02 +02:00
Compare commits
528 Commits
tts-cleanu
...
assist-pip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa4740973 | ||
|
|
97084e9382 | ||
|
|
9db34fe232 | ||
|
|
c4f0b4ab23 | ||
|
|
1647afc58a | ||
|
|
53ea8422f8 | ||
|
|
0b988b3fac | ||
|
|
5a4abe3ec1 | ||
|
|
89abc5ac69 | ||
|
|
08fe6653bb | ||
|
|
9aa18c7157 | ||
|
|
cc7929f8fb | ||
|
|
d657298791 | ||
|
|
05f393560f | ||
|
|
92da640d4c | ||
|
|
ad3fd151aa | ||
|
|
cd104dc08c | ||
|
|
d3745d2519 | ||
|
|
931f3fa41a | ||
|
|
87b5a91212 | ||
|
|
3b8da62d84 | ||
|
|
86a48294f4 | ||
|
|
a03884981f | ||
|
|
ab695f90c7 | ||
|
|
efcf8f9555 | ||
|
|
f71903a563 | ||
|
|
95552e9a5b | ||
|
|
5da57271b2 | ||
|
|
62a7139f4d | ||
|
|
a7be26cd95 | ||
|
|
9c3b0952e0 | ||
|
|
c771f446b4 | ||
|
|
9a25561017 | ||
|
|
bd870f0537 | ||
|
|
d7f43bddfa | ||
|
|
87107c5a59 | ||
|
|
9ce920b35a | ||
|
|
15aff9662c | ||
|
|
da6fb91886 | ||
|
|
1e880f7406 | ||
|
|
81153042d3 | ||
|
|
493ca261dc | ||
|
|
7493b340ca | ||
|
|
e85e60ed6a | ||
|
|
8ff4d5dcbf | ||
|
|
f2838e493b | ||
|
|
a71edcf1a1 | ||
|
|
47bef74e7c | ||
|
|
b757a7e3fe | ||
|
|
362ff5724d | ||
|
|
4f8363a5c2 | ||
|
|
ae3925118c | ||
|
|
b2fcab20a6 | ||
|
|
6423957d29 | ||
|
|
835cdad0a9 | ||
|
|
d8d6decb38 | ||
|
|
16b42cc109 | ||
|
|
a47f27821f | ||
|
|
c797e7a973 | ||
|
|
245eb64405 | ||
|
|
a895fcf057 | ||
|
|
5706fb26b8 | ||
|
|
3f82120cdc | ||
|
|
20df183470 | ||
|
|
980216795f | ||
|
|
fdfcd841ba | ||
|
|
28a09794e9 | ||
|
|
a0c9217375 | ||
|
|
469176c59b | ||
|
|
3ece672890 | ||
|
|
c6ebba8843 | ||
|
|
1f047807a4 | ||
|
|
f1b724c49a | ||
|
|
5ebed2046c | ||
|
|
d1236a53b8 | ||
|
|
84f07ee992 | ||
|
|
d7f5e48626 | ||
|
|
56e07bb1f2 | ||
|
|
45b2700375 | ||
|
|
d860b35f41 | ||
|
|
5392062edd | ||
|
|
d9a09a2aea | ||
|
|
3daff73d36 | ||
|
|
e6b88ec087 | ||
|
|
592dcec852 | ||
|
|
2a6b79ec0f | ||
|
|
afc1d224a0 | ||
|
|
b668acda24 | ||
|
|
c3996d6931 | ||
|
|
9ec174776c | ||
|
|
6a8722cf7c | ||
|
|
000b1d80b0 | ||
|
|
6d8654610e | ||
|
|
5cd4c8e896 | ||
|
|
cec8db173b | ||
|
|
dd9dad80be | ||
|
|
9992ade051 | ||
|
|
36da4a9b72 | ||
|
|
3fc34244ac | ||
|
|
753c07e911 | ||
|
|
d0850e2931 | ||
|
|
c704df004a | ||
|
|
d95c9c496e | ||
|
|
d28f4ed618 | ||
|
|
7a0580eff5 | ||
|
|
f94af84f2a | ||
|
|
31fb199670 | ||
|
|
a1ca0a1cb2 | ||
|
|
2326c23133 | ||
|
|
d4c1d1bdb9 | ||
|
|
8d258871ff | ||
|
|
49299a6bf0 | ||
|
|
868b8ad318 | ||
|
|
40752dcfb6 | ||
|
|
18f51abfe6 | ||
|
|
3e2c54dcbd | ||
|
|
a0cd14b4e8 | ||
|
|
35c6fdbce8 | ||
|
|
202addc39d | ||
|
|
d8cb7c475b | ||
|
|
03bacd747e | ||
|
|
97b6a68cda | ||
|
|
eee18035cf | ||
|
|
f1b3b0c155 | ||
|
|
f5d3495c62 | ||
|
|
e14a356c24 | ||
|
|
4e7d396e5b | ||
|
|
34d17ca458 | ||
|
|
03950f270a | ||
|
|
7074331461 | ||
|
|
4c9cd70f65 | ||
|
|
7a105de969 | ||
|
|
eec9a28fe8 | ||
|
|
963f1b1907 | ||
|
|
dcac9b5f20 | ||
|
|
765a95c273 | ||
|
|
6a115d0133 | ||
|
|
a057effad5 | ||
|
|
94b0800989 | ||
|
|
a783b6a0ab | ||
|
|
5302964eb6 | ||
|
|
261dbd16a6 | ||
|
|
672dbc03c6 | ||
|
|
ed0bdf9e5f | ||
|
|
735e2e4192 | ||
|
|
0aabb11220 | ||
|
|
09ad14bc28 | ||
|
|
d61e39743b | ||
|
|
ea90df434b | ||
|
|
67fc682df2 | ||
|
|
381b495efc | ||
|
|
812db815f1 | ||
|
|
24ee19f1e2 | ||
|
|
f72c5ebb76 | ||
|
|
1075ea1220 | ||
|
|
ce7edca136 | ||
|
|
3e16857a1e | ||
|
|
5b1e32f51d | ||
|
|
4adf5ce826 | ||
|
|
4a1905a2a2 | ||
|
|
59af3a396c | ||
|
|
7c584ece23 | ||
|
|
ff2c901930 | ||
|
|
dc8e1773f1 | ||
|
|
b0d9a2437d | ||
|
|
2be6ecd50f | ||
|
|
fa0bb35e6c | ||
|
|
360bffa3a9 | ||
|
|
5b503f21d7 | ||
|
|
2214d9b330 | ||
|
|
6a2d733d85 | ||
|
|
7392d5a30a | ||
|
|
b3deeca939 | ||
|
|
c38a3a239c | ||
|
|
afa6ed09ef | ||
|
|
deb966128f | ||
|
|
73707fa231 | ||
|
|
10ac39f6b2 | ||
|
|
2e05dc8618 | ||
|
|
d8233b4de5 | ||
|
|
7cbc3ea65f | ||
|
|
cb0523660d | ||
|
|
605bf7e287 | ||
|
|
3405b2549b | ||
|
|
d83c617566 | ||
|
|
7016c19b2f | ||
|
|
5cd4a0ced6 | ||
|
|
347c1a2141 | ||
|
|
46eae64ef6 | ||
|
|
a74fe60b91 | ||
|
|
fab70a80bb | ||
|
|
2abe2f7d59 | ||
|
|
cc970354d7 | ||
|
|
e389ff2537 | ||
|
|
088f0c82bd | ||
|
|
fa1bb27dd2 | ||
|
|
5a6ce34352 | ||
|
|
fdcb88977a | ||
|
|
a584ccb8f7 | ||
|
|
cc290b15f6 | ||
|
|
575db4665d | ||
|
|
a61aff8432 | ||
|
|
3aa1c60fe3 | ||
|
|
39f3aa7e78 | ||
|
|
01e2c3272b | ||
|
|
5afcd3e54e | ||
|
|
b081064954 | ||
|
|
11e63ca96a | ||
|
|
6457d46107 | ||
|
|
987bf4d850 | ||
|
|
fa80c0a88d | ||
|
|
f69484ba02 | ||
|
|
11f63c7868 | ||
|
|
3245124553 | ||
|
|
44475967eb | ||
|
|
2d27b5ac53 | ||
|
|
2ae161d8b5 | ||
|
|
aefe83b1a3 | ||
|
|
f86e85b931 | ||
|
|
993ebc9eba | ||
|
|
1d99bbf22e | ||
|
|
eb4fa635bf | ||
|
|
49522d93df | ||
|
|
9e0a7122f5 | ||
|
|
e4fe7ba985 | ||
|
|
f3ea11bbc1 | ||
|
|
55de91530d | ||
|
|
290bbcfa3e | ||
|
|
061a1be2bc | ||
|
|
4bd8c319dd | ||
|
|
367022dd8c | ||
|
|
f1975d9dbf | ||
|
|
0764cf1165 | ||
|
|
a55a6e5c48 | ||
|
|
5230aa8917 | ||
|
|
f7e3e207b7 | ||
|
|
6f0c59f1be | ||
|
|
5fcdbd7742 | ||
|
|
4173ff5339 | ||
|
|
e8c4d08b25 | ||
|
|
36081c69e0 | ||
|
|
65db3c1164 | ||
|
|
11f02e48d7 | ||
|
|
e41283a40a | ||
|
|
738e39413d | ||
|
|
8df0a950f7 | ||
|
|
199a274c80 | ||
|
|
731d1ab796 | ||
|
|
f6d8868eb6 | ||
|
|
253cc377b4 | ||
|
|
1bfd585f3c | ||
|
|
839eb0fe14 | ||
|
|
3dcd06806d | ||
|
|
3c174ce329 | ||
|
|
8a2347539c | ||
|
|
f22eca3d9e | ||
|
|
3cb301214f | ||
|
|
8215faea0d | ||
|
|
beab4e0d7c | ||
|
|
b785d5297a | ||
|
|
d86d7b8843 | ||
|
|
2ca5f05555 | ||
|
|
e95188059f | ||
|
|
73e6c8541c | ||
|
|
6f9c8b2aa0 | ||
|
|
2d20df37b1 | ||
|
|
0b2e5cd253 | ||
|
|
0208188bb5 | ||
|
|
44ae596929 | ||
|
|
db0cbf1ea9 | ||
|
|
896da4abbd | ||
|
|
731e9bbbfc | ||
|
|
bf1c138a3c | ||
|
|
00fc3e2c29 | ||
|
|
a258aa50a5 | ||
|
|
3cf12a4792 | ||
|
|
e56f6fafdc | ||
|
|
e9789e0b3e | ||
|
|
6f0a9910ea | ||
|
|
b8793760a1 | ||
|
|
6264f9c67b | ||
|
|
2a74deb84e | ||
|
|
9d1ff37a79 | ||
|
|
2f99164781 | ||
|
|
80ef32f09d | ||
|
|
63be0e2e1a | ||
|
|
74c4553bb0 | ||
|
|
e240707b32 | ||
|
|
7c867852a9 | ||
|
|
2d149dc746 | ||
|
|
7edcddd3e4 | ||
|
|
71f658b560 | ||
|
|
9886db5d6d | ||
|
|
c236cd070c | ||
|
|
9f1a830d32 | ||
|
|
1e69ce9111 | ||
|
|
389297155d | ||
|
|
c341b86520 | ||
|
|
88eef379b2 | ||
|
|
34767d4058 | ||
|
|
12c3d54a63 | ||
|
|
33a185dade | ||
|
|
c1c5776d85 | ||
|
|
eda642554d | ||
|
|
51f5ce013f | ||
|
|
f7794ea6b5 | ||
|
|
7a1bea7ff5 | ||
|
|
c7c645776d | ||
|
|
667cb772e9 | ||
|
|
933d008e52 | ||
|
|
d868f39aea | ||
|
|
28d776a0b0 | ||
|
|
b5d541b596 | ||
|
|
4948499889 | ||
|
|
7696b101f6 | ||
|
|
fd2987a9fd | ||
|
|
4c1d32020a | ||
|
|
b40bdab0ae | ||
|
|
d192aecd3b | ||
|
|
d1781f5766 | ||
|
|
2c4461457a | ||
|
|
82959081de | ||
|
|
acdac6d5e8 | ||
|
|
d3d7889883 | ||
|
|
60ece3e1c9 | ||
|
|
a9f8529460 | ||
|
|
ec53b61f9e | ||
|
|
e9f02edd8b | ||
|
|
d1b7898219 | ||
|
|
8dc21ef619 | ||
|
|
d9f91598a5 | ||
|
|
c540acf2bd | ||
|
|
f702f3efcd | ||
|
|
9410061405 | ||
|
|
485b28d9ea | ||
|
|
d59200a9f5 | ||
|
|
44a92ca81c | ||
|
|
d39fa39a03 | ||
|
|
36ec857523 | ||
|
|
fcb8cdc146 | ||
|
|
2322b0b65f | ||
|
|
87baaf4255 | ||
|
|
b7f0e877f0 | ||
|
|
5d92a04732 | ||
|
|
8ff879df22 | ||
|
|
9fb7ee676e | ||
|
|
2c855a3986 | ||
|
|
cdd4894e30 | ||
|
|
5f26226712 | ||
|
|
8baf61031d | ||
|
|
e90ba40553 | ||
|
|
b38016425f | ||
|
|
ee5e3f7691 | ||
|
|
7af6a4f493 | ||
|
|
c25f26a290 | ||
|
|
8d62cb60a6 | ||
|
|
4f799069ea | ||
|
|
af708b78e0 | ||
|
|
f46e659740 | ||
|
|
7bd517e6ff | ||
|
|
e9abdab1f5 | ||
|
|
86eee4f041 | ||
|
|
9db60c830c | ||
|
|
c43a4682b9 | ||
|
|
2a4996055a | ||
|
|
4643fc2c14 | ||
|
|
6410b90d82 | ||
|
|
e5c00eceae | ||
|
|
fe65579df8 | ||
|
|
281beecb05 | ||
|
|
7546b5d269 | ||
|
|
490e3201b9 | ||
|
|
04be575139 | ||
|
|
854cae7f12 | ||
|
|
109d20978f | ||
|
|
f8d284ec4b | ||
|
|
06ebe0810f | ||
|
|
802ad2ff51 | ||
|
|
9070a8d579 | ||
|
|
e8b2a3de8b | ||
|
|
39549d5dd4 | ||
|
|
0c19e47bd4 | ||
|
|
05507d77e3 | ||
|
|
94558e2d40 | ||
|
|
4f22fe8f7f | ||
|
|
9e7dfbb857 | ||
|
|
02d182239a | ||
|
|
4e0f581747 | ||
|
|
42d97d348c | ||
|
|
69380c85ca | ||
|
|
b38c647830 | ||
|
|
2396fd1090 | ||
|
|
aa4eb89eee | ||
|
|
1b1bc6af95 | ||
|
|
f17003a79c | ||
|
|
ec70e8b0cd | ||
|
|
d888c70ff0 | ||
|
|
f29444002e | ||
|
|
fc66997a36 | ||
|
|
35513ae072 | ||
|
|
cd363d48c3 | ||
|
|
d47ef835d7 | ||
|
|
00177c699e | ||
|
|
11b0086a01 | ||
|
|
ceb177f80e | ||
|
|
fa3832fbd7 | ||
|
|
2b9c903429 | ||
|
|
a7c43f9b49 | ||
|
|
b428196149 | ||
|
|
e23da1a90f | ||
|
|
3951c2ea66 | ||
|
|
fee152654d | ||
|
|
51073c948c | ||
|
|
91438088a0 | ||
|
|
427e1abdae | ||
|
|
6e7ac45ac0 | ||
|
|
4b3b9ebc29 | ||
|
|
649d8638ed | ||
|
|
12c4152dbe | ||
|
|
8f9572bb05 | ||
|
|
6d022ff4e0 | ||
|
|
c0c2edb90a | ||
|
|
b014219fdd | ||
|
|
216b8ef400 | ||
|
|
f2ccd46267 | ||
|
|
e16ba27ce8 | ||
|
|
506526a6a2 | ||
|
|
a88678cf42 | ||
|
|
d0b61af7ec | ||
|
|
04f5315ab2 | ||
|
|
7f9e4ba39e | ||
|
|
06aaf188ea | ||
|
|
627f994872 | ||
|
|
9e81ec5aae | ||
|
|
69753fca1d | ||
|
|
7773cc121e | ||
|
|
3aa56936ad | ||
|
|
e66416c23d | ||
|
|
a592feae3d | ||
|
|
fc0d71e891 | ||
|
|
d4640f1d24 | ||
|
|
6fe158836e | ||
|
|
629c0087f4 | ||
|
|
363bd75129 | ||
|
|
7592d350a8 | ||
|
|
8ac8401b4e | ||
|
|
eed075dbfa | ||
|
|
23dbdedfb6 | ||
|
|
85ad29e28e | ||
|
|
35fc81b038 | ||
|
|
5d45b84cd2 | ||
|
|
7766649304 | ||
|
|
07e9020dfa | ||
|
|
f504a759e0 | ||
|
|
9927de4801 | ||
|
|
1244fc4682 | ||
|
|
e77a1b12f7 | ||
|
|
5459daaa10 | ||
|
|
400131df78 | ||
|
|
28e1843ff9 | ||
|
|
df777318d1 | ||
|
|
6ad5e9e89c | ||
|
|
a0bd8deee9 | ||
|
|
405cbd6a00 | ||
|
|
3e0eb5ab2c | ||
|
|
fad75a70b6 | ||
|
|
d9720283df | ||
|
|
14eed1778b | ||
|
|
049aaa7e8b | ||
|
|
35717e8216 | ||
|
|
2a081abc18 | ||
|
|
b7f29c7358 | ||
|
|
3bb6373df5 | ||
|
|
e1b4edec50 | ||
|
|
147bee57e1 | ||
|
|
fcdaea64da | ||
|
|
d1512d46be | ||
|
|
0be7db6270 | ||
|
|
2af0282725 | ||
|
|
ff458c8417 | ||
|
|
cc93152ff0 | ||
|
|
9965f01609 | ||
|
|
e9c76ce694 | ||
|
|
58ab7d350d | ||
|
|
e4d6e20ebd | ||
|
|
45e273897a | ||
|
|
d9ec7142d7 | ||
|
|
e162499267 | ||
|
|
67f21429e3 | ||
|
|
a0563f06c9 | ||
|
|
e7c4fdc8bb | ||
|
|
c490e350bc | ||
|
|
e11409ef99 | ||
|
|
5c8e415a76 | ||
|
|
e795fb9497 | ||
|
|
d0afabb85c | ||
|
|
4f3e8e9b94 | ||
|
|
46c1cbbc9c | ||
|
|
8d9a4ea278 | ||
|
|
22c83e2393 | ||
|
|
c83a75f6f9 | ||
|
|
841c727112 | ||
|
|
d8c9655bfd | ||
|
|
942ed89cc4 | ||
|
|
a1fe6b9cf3 | ||
|
|
2567181cc2 | ||
|
|
028e4f6029 | ||
|
|
b82e1a9bef | ||
|
|
438f226c31 | ||
|
|
2f139e3cb1 | ||
|
|
5d75e96fbf | ||
|
|
dcf2ec5c37 | ||
|
|
2431e1ba98 | ||
|
|
4ead108c15 | ||
|
|
ec8363fa49 | ||
|
|
e7ff0a3f8b | ||
|
|
f4c0eb4189 | ||
|
|
b1ee5a76e1 | ||
|
|
6b9e8c301b | ||
|
|
89c3266c7e | ||
|
|
cff0a632e8 | ||
|
|
e04d8557ae | ||
|
|
ca6286f241 | ||
|
|
35bcc9d5af | ||
|
|
25b45ce867 | ||
|
|
d568209bd5 | ||
|
|
8a43e8af9e | ||
|
|
785e5b2c16 |
18
.github/workflows/builder.yml
vendored
18
.github/workflows/builder.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
uses: sigstore/cosign-installer@v3.8.2
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
@@ -457,12 +457,12 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
40
.github/workflows/ci.yaml
vendored
40
.github/workflows/ci.yaml
vendored
@@ -249,7 +249,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -334,7 +334,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -484,7 +484,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -587,7 +587,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -620,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -677,7 +677,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -767,7 +767,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -812,7 +812,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -949,7 +949,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -968,7 +968,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1074,7 +1074,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1208,7 +1208,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1312,7 +1312,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1359,7 +1359,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1454,7 +1454,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1479,7 +1479,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.15
|
||||
uses: github/codeql-action/init@v3.28.16
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.15
|
||||
uses: github/codeql-action/analyze@v3.28.16
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
16
.github/workflows/wheels.yml
vendored
16
.github/workflows/wheels.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -138,17 +138,17 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -187,22 +187,22 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
||||
@@ -363,6 +363,7 @@ homeassistant.components.no_ip.*
|
||||
homeassistant.components.nordpool.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.ntfy.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
homeassistant.components.ohme.*
|
||||
@@ -385,6 +386,7 @@ homeassistant.components.pandora.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.peblar.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.pegel_online.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
@@ -461,6 +463,7 @@ homeassistant.components.slack.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.smlight.*
|
||||
homeassistant.components.smtp.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
|
||||
11
CODEOWNERS
generated
11
CODEOWNERS
generated
@@ -1051,6 +1051,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/nsw_fuel_station/ @nickw444
|
||||
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
/homeassistant/components/ntfy/ @tr4nt0r
|
||||
/tests/components/ntfy/ @tr4nt0r
|
||||
/homeassistant/components/nuheat/ @tstabrawa
|
||||
/tests/components/nuheat/ @tstabrawa
|
||||
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
|
||||
@@ -1316,6 +1318,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruuvitag_ble/ @akx
|
||||
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/homeassistant/components/s3/ @tomasbedrich
|
||||
/tests/components/s3/ @tomasbedrich
|
||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
@@ -1437,8 +1441,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
||||
/tests/components/soma/ @ratsept @sebfortier2288
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
/tests/components/soma/ @ratsept
|
||||
/homeassistant/components/sonarr/ @ctalkington
|
||||
/tests/components/sonarr/ @ctalkington
|
||||
/homeassistant/components/songpal/ @rytilahti @shenxn
|
||||
@@ -1470,7 +1474,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/steam_online/ @tkdrob
|
||||
/homeassistant/components/steamist/ @bdraco
|
||||
/tests/components/steamist/ @bdraco
|
||||
/homeassistant/components/stiebel_eltron/ @fucm
|
||||
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/homeassistant/components/stookwijzer/ @fwestenberg
|
||||
/tests/components/stookwijzer/ @fwestenberg
|
||||
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
||||
|
||||
@@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||
class MediaPlayerCapabilities(AlexaEntity):
|
||||
"""Class to represent MediaPlayer capabilities."""
|
||||
|
||||
@@ -757,9 +757,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
|
||||
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
|
||||
inputs = AlexaInputController.get_valid_inputs(
|
||||
self.entity.attributes.get(
|
||||
media_player.const.ATTR_INPUT_SOURCE_LIST, []
|
||||
)
|
||||
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaInputController(self.entity)
|
||||
@@ -776,8 +774,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
and domain != "denonavr"
|
||||
):
|
||||
inputs = AlexaEqualizerController.get_valid_inputs(
|
||||
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
or []
|
||||
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaEqualizerController(self.entity)
|
||||
|
||||
@@ -566,7 +566,7 @@ async def async_api_set_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -589,7 +589,7 @@ async def async_api_select_input(
|
||||
|
||||
# Attempt to map the ALL UPPERCASE payload name to a source.
|
||||
# Strips trailing 1 to match single input devices.
|
||||
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
|
||||
source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
|
||||
for source in source_list:
|
||||
formatted_source = (
|
||||
source.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||||
@@ -611,7 +611,7 @@ async def async_api_select_input(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
media_player.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -636,7 +636,7 @@ async def async_api_adjust_volume(
|
||||
volume_delta = int(directive.payload["volume"])
|
||||
|
||||
entity = directive.entity
|
||||
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
|
||||
# read current state
|
||||
try:
|
||||
@@ -648,7 +648,7 @@ async def async_api_adjust_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -709,7 +709,7 @@ async def async_api_set_mute(
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1708,15 +1708,13 @@ async def async_api_changechannel(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
media_player.const.MEDIA_TYPE_CHANNEL
|
||||
),
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
media_player.const.SERVICE_PLAY_MEDIA,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
@@ -1825,13 +1823,13 @@ async def async_api_set_eq_mode(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
mode: str = directive.payload["mode"]
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
||||
data[media_player.ATTR_SOUND_MODE] = mode.lower()
|
||||
else:
|
||||
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -260,10 +260,10 @@ async def async_enable_proactive_mode(
|
||||
def extra_significant_check(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
|
||||
old_attrs: Mapping[Any, Any],
|
||||
old_extra_arg: Any,
|
||||
new_state: str,
|
||||
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
|
||||
new_attrs: Mapping[Any, Any],
|
||||
new_extra_arg: Any,
|
||||
) -> bool:
|
||||
"""Check if the serialized data has changed."""
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_addons": "Addons",
|
||||
"tracked_addons": "Add-ons",
|
||||
"tracked_integrations": "Integrations",
|
||||
"tracked_custom_integrations": "Custom integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "Select the addons you want to track",
|
||||
"tracked_addons": "Select the add-ons you want to track",
|
||||
"tracked_integrations": "Select the integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
@@ -175,7 +176,7 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
|
||||
def anthropic_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
||||
options: Mapping[str, Any],
|
||||
) -> dict:
|
||||
"""Return a schema for Anthropic completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
|
||||
@@ -9,11 +9,13 @@ from anthropic import AsyncStream
|
||||
from anthropic._types import NOT_GIVEN
|
||||
from anthropic.types import (
|
||||
InputJSONDelta,
|
||||
MessageDeltaUsage,
|
||||
MessageParam,
|
||||
MessageStreamEvent,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
RawMessageDeltaEvent,
|
||||
RawMessageStartEvent,
|
||||
RawMessageStopEvent,
|
||||
RedactedThinkingBlock,
|
||||
@@ -31,6 +33,7 @@ from anthropic.types import (
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlock,
|
||||
ToolUseBlockParam,
|
||||
Usage,
|
||||
)
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -162,7 +165,8 @@ def _convert_content(
|
||||
return messages
|
||||
|
||||
|
||||
async def _transform_stream(
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
chat_log: conversation.ChatLog,
|
||||
result: AsyncStream[MessageStreamEvent],
|
||||
messages: list[MessageParam],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
@@ -207,6 +211,7 @@ async def _transform_stream(
|
||||
| None
|
||||
) = None
|
||||
current_tool_args: str
|
||||
input_usage: Usage | None = None
|
||||
|
||||
async for response in result:
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
@@ -215,6 +220,7 @@ async def _transform_stream(
|
||||
if response.message.role != "assistant":
|
||||
raise ValueError("Unexpected message role")
|
||||
current_message = MessageParam(role=response.message.role, content=[])
|
||||
input_usage = response.message.usage
|
||||
elif isinstance(response, RawContentBlockStartEvent):
|
||||
if isinstance(response.content_block, ToolUseBlock):
|
||||
current_block = ToolUseBlockParam(
|
||||
@@ -285,12 +291,34 @@ async def _transform_stream(
|
||||
raise ValueError("Unexpected stop event without a current message")
|
||||
current_message["content"].append(current_block) # type: ignore[union-attr]
|
||||
current_block = None
|
||||
elif isinstance(response, RawMessageDeltaEvent):
|
||||
if (usage := response.usage) is not None:
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if current_message is not None:
|
||||
messages.append(current_message)
|
||||
current_message = None
|
||||
|
||||
|
||||
def _create_token_stats(
|
||||
input_usage: Usage | None, response_usage: MessageDeltaUsage
|
||||
) -> dict[str, Any]:
|
||||
"""Create token stats for conversation agent tracing."""
|
||||
input_tokens = 0
|
||||
cached_input_tokens = 0
|
||||
if input_usage:
|
||||
input_tokens = input_usage.input_tokens
|
||||
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
|
||||
output_tokens = response_usage.output_tokens
|
||||
return {
|
||||
"stats": {
|
||||
"input_tokens": input_tokens,
|
||||
"cached_input_tokens": cached_input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnthropicConversationEntity(
|
||||
conversation.ConversationEntity, conversation.AbstractConversationAgent
|
||||
):
|
||||
@@ -393,7 +421,8 @@ class AnthropicConversationEntity(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
user_input.agent_id, _transform_stream(stream, messages)
|
||||
user_input.agent_id,
|
||||
_transform_stream(chat_log, stream, messages),
|
||||
)
|
||||
if not isinstance(content, conversation.AssistantContent)
|
||||
]
|
||||
|
||||
@@ -113,4 +113,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
||||
data = await aioapcaccess.request_status(self._host, self._port)
|
||||
return APCUPSdData(data)
|
||||
except (OSError, asyncio.IncompleteReadError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from error
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"name": "Internal temperature"
|
||||
},
|
||||
"last_self_test": {
|
||||
"name": "Last self test"
|
||||
"name": "Last self-test"
|
||||
},
|
||||
"last_transfer": {
|
||||
"name": "Last transfer"
|
||||
@@ -177,7 +177,7 @@
|
||||
"name": "Restore requirement"
|
||||
},
|
||||
"self_test_result": {
|
||||
"name": "Self test result"
|
||||
"name": "Self-test result"
|
||||
},
|
||||
"sensitivity": {
|
||||
"name": "Sensitivity"
|
||||
@@ -195,7 +195,7 @@
|
||||
"name": "Status"
|
||||
},
|
||||
"self_test_interval": {
|
||||
"name": "Self test interval"
|
||||
"name": "Self-test interval"
|
||||
},
|
||||
"time_left": {
|
||||
"name": "Time left"
|
||||
@@ -219,5 +219,10 @@
|
||||
"name": "Transfer to battery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to APC UPS Daemon."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.5.0"]
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ class AssistSatelliteAnnouncement:
|
||||
media_id: str
|
||||
"""Media ID to be played."""
|
||||
|
||||
original_media_id: str
|
||||
"""The raw media ID before processing."""
|
||||
|
||||
tts_token: str | None
|
||||
"""The TTS token of the media."""
|
||||
|
||||
@@ -498,7 +501,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
media_id_source: Literal["url", "media_id", "tts"] | None = None
|
||||
tts_token: str | None = None
|
||||
|
||||
if not media_id:
|
||||
if media_id:
|
||||
original_media_id = media_id
|
||||
else:
|
||||
media_id_source = "tts"
|
||||
# Synthesize audio and get URL
|
||||
pipeline_id = self._resolve_pipeline()
|
||||
@@ -525,6 +530,13 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
|
||||
tts_token = stream.token
|
||||
media_id = stream.url
|
||||
original_media_id = tts.generate_media_source_id(
|
||||
self.hass,
|
||||
message,
|
||||
engine=engine,
|
||||
language=pipeline.tts_language,
|
||||
options=tts_options,
|
||||
)
|
||||
|
||||
if media_source.is_media_source_id(media_id):
|
||||
if not media_id_source:
|
||||
@@ -560,6 +572,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
return AssistSatelliteAnnouncement(
|
||||
message=message,
|
||||
media_id=media_id,
|
||||
original_media_id=original_media_id,
|
||||
tts_token=tts_token,
|
||||
media_id_source=media_id_source,
|
||||
preannounce_media_id=preannounce_media_id,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
@@ -363,7 +362,7 @@ class AsusWrtRouter:
|
||||
"""Add a function to call when router is closed."""
|
||||
self._on_close.append(func)
|
||||
|
||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> bool:
|
||||
def update_options(self, new_options: Mapping[str, Any]) -> bool:
|
||||
"""Update router options."""
|
||||
req_reload = False
|
||||
for name, new_opt in new_options.items():
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -27,6 +28,7 @@ from homeassistant.const import (
|
||||
CONF_MODE,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
@@ -86,11 +88,9 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .config import AutomationConfig, ValidationStatus
|
||||
from .const import (
|
||||
CONF_ACTIONS,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DEFAULT_INITIAL_STATE,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
||||
@@ -14,11 +14,15 @@ from homeassistant.components import blueprint
|
||||
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
|
||||
from homeassistant.config import config_per_platform, config_without_domain
|
||||
from homeassistant.const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -30,14 +34,10 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.yaml.input import UndefinedSubstitution
|
||||
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_HIDE_ENTITY,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
@@ -58,34 +58,9 @@ _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
|
||||
def _backward_compat_schema(value: Any | None) -> Any:
|
||||
"""Backward compatibility for automations."""
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# `trigger` has been renamed to `triggers`
|
||||
if CONF_TRIGGER in value:
|
||||
if CONF_TRIGGERS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
|
||||
)
|
||||
value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
|
||||
|
||||
# `condition` has been renamed to `conditions`
|
||||
if CONF_CONDITION in value:
|
||||
if CONF_CONDITIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
|
||||
)
|
||||
value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
|
||||
|
||||
# `action` has been renamed to `actions`
|
||||
if CONF_ACTION in value:
|
||||
if CONF_ACTIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'action' and 'actions'. Please use 'actions' only."
|
||||
)
|
||||
value[CONF_ACTIONS] = value.pop(CONF_ACTION)
|
||||
|
||||
return value
|
||||
value = cv.renamed(CONF_TRIGGER, CONF_TRIGGERS)(value)
|
||||
value = cv.renamed(CONF_ACTION, CONF_ACTIONS)(value)
|
||||
return cv.renamed(CONF_CONDITION, CONF_CONDITIONS)(value)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ACTIONS = "actions"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_TRIGGERS = "triggers"
|
||||
CONF_TRIGGER_VARIABLES = "trigger_variables"
|
||||
DOMAIN = "automation"
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -88,7 +87,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
api = await get_axis_api(self.hass, MappingProxyType(user_input))
|
||||
api = await get_axis_api(self.hass, user_input)
|
||||
|
||||
except AuthenticationRequired:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Axis network device abstraction."""
|
||||
|
||||
from asyncio import timeout
|
||||
from types import MappingProxyType
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import axis
|
||||
@@ -23,7 +23,7 @@ from ..errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
async def get_axis_api(
|
||||
hass: HomeAssistant,
|
||||
config: MappingProxyType[str, Any],
|
||||
config: Mapping[str, Any],
|
||||
) -> axis.AxisDevice:
|
||||
"""Create a Axis device API."""
|
||||
session = get_async_client(hass, verify_ssl=False)
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from azure.eventhub import EventData, EventDataBatch
|
||||
@@ -179,7 +178,7 @@ class AzureEventHub:
|
||||
await self.async_send(None)
|
||||
await self._queue.join()
|
||||
|
||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> None:
|
||||
def update_options(self, new_options: Mapping[str, Any]) -> None:
|
||||
"""Update options."""
|
||||
self._send_interval = new_options[CONF_SEND_INTERVAL]
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field, replace
|
||||
import datetime as dt
|
||||
from datetime import datetime, timedelta
|
||||
@@ -87,12 +88,26 @@ class BackupConfigData:
|
||||
else:
|
||||
time = None
|
||||
days = [Day(day) for day in data["schedule"]["days"]]
|
||||
agents = {}
|
||||
for agent_id, agent_data in data["agents"].items():
|
||||
protected = agent_data["protected"]
|
||||
stored_retention = agent_data["retention"]
|
||||
agent_retention: AgentRetentionConfig | None
|
||||
if stored_retention:
|
||||
agent_retention = AgentRetentionConfig(
|
||||
copies=stored_retention["copies"],
|
||||
days=stored_retention["days"],
|
||||
)
|
||||
else:
|
||||
agent_retention = None
|
||||
agent_config = AgentConfig(
|
||||
protected=protected,
|
||||
retention=agent_retention,
|
||||
)
|
||||
agents[agent_id] = agent_config
|
||||
|
||||
return cls(
|
||||
agents={
|
||||
agent_id: AgentConfig(protected=agent_data["protected"])
|
||||
for agent_id, agent_data in data["agents"].items()
|
||||
},
|
||||
agents=agents,
|
||||
automatic_backups_configured=data["automatic_backups_configured"],
|
||||
create_backup=CreateBackupConfig(
|
||||
agent_ids=data["create_backup"]["agent_ids"],
|
||||
@@ -176,12 +191,36 @@ class BackupConfig:
|
||||
"""Update config."""
|
||||
if agents is not UNDEFINED:
|
||||
for agent_id, agent_config in agents.items():
|
||||
if agent_id not in self.data.agents:
|
||||
self.data.agents[agent_id] = AgentConfig(**agent_config)
|
||||
agent_retention = agent_config.get("retention")
|
||||
if agent_retention is None:
|
||||
new_agent_retention = None
|
||||
else:
|
||||
self.data.agents[agent_id] = replace(
|
||||
self.data.agents[agent_id], **agent_config
|
||||
new_agent_retention = AgentRetentionConfig(
|
||||
copies=agent_retention.get("copies"),
|
||||
days=agent_retention.get("days"),
|
||||
)
|
||||
if agent_id not in self.data.agents:
|
||||
old_agent_retention = None
|
||||
self.data.agents[agent_id] = AgentConfig(
|
||||
protected=agent_config.get("protected", False),
|
||||
retention=new_agent_retention,
|
||||
)
|
||||
else:
|
||||
new_agent_config = self.data.agents[agent_id]
|
||||
old_agent_retention = new_agent_config.retention
|
||||
if "protected" in agent_config:
|
||||
new_agent_config = replace(
|
||||
new_agent_config, protected=agent_config["protected"]
|
||||
)
|
||||
if "retention" in agent_config:
|
||||
new_agent_config = replace(
|
||||
new_agent_config, retention=new_agent_retention
|
||||
)
|
||||
self.data.agents[agent_id] = new_agent_config
|
||||
if new_agent_retention != old_agent_retention:
|
||||
# There's a single retention application method
|
||||
# for both global and agent retention settings.
|
||||
self.data.retention.apply(self._manager)
|
||||
if automatic_backups_configured is not UNDEFINED:
|
||||
self.data.automatic_backups_configured = automatic_backups_configured
|
||||
if create_backup is not UNDEFINED:
|
||||
@@ -207,11 +246,24 @@ class AgentConfig:
|
||||
"""Represent the config for an agent."""
|
||||
|
||||
protected: bool
|
||||
"""Agent protected configuration.
|
||||
|
||||
If True, the agent backups are password protected.
|
||||
"""
|
||||
retention: AgentRetentionConfig | None = None
|
||||
"""Agent retention configuration.
|
||||
|
||||
If None, the global retention configuration is used.
|
||||
If not None, the global retention configuration is ignored for this agent.
|
||||
If an agent retention configuration is set and both copies and days are None,
|
||||
backups will be kept forever for that agent.
|
||||
"""
|
||||
|
||||
def to_dict(self) -> StoredAgentConfig:
|
||||
"""Convert agent config to a dict."""
|
||||
return {
|
||||
"protected": self.protected,
|
||||
"retention": self.retention.to_dict() if self.retention else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -219,24 +271,46 @@ class StoredAgentConfig(TypedDict):
|
||||
"""Represent the stored config for an agent."""
|
||||
|
||||
protected: bool
|
||||
retention: StoredRetentionConfig | None
|
||||
|
||||
|
||||
class AgentParametersDict(TypedDict, total=False):
|
||||
"""Represent the parameters for an agent."""
|
||||
|
||||
protected: bool
|
||||
retention: RetentionParametersDict | None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RetentionConfig:
|
||||
"""Represent the backup retention configuration."""
|
||||
class BaseRetentionConfig:
|
||||
"""Represent the base backup retention configuration."""
|
||||
|
||||
copies: int | None = None
|
||||
days: int | None = None
|
||||
|
||||
def to_dict(self) -> StoredRetentionConfig:
|
||||
"""Convert backup retention configuration to a dict."""
|
||||
return StoredRetentionConfig(
|
||||
copies=self.copies,
|
||||
days=self.days,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RetentionConfig(BaseRetentionConfig):
|
||||
"""Represent the backup retention configuration."""
|
||||
|
||||
def apply(self, manager: BackupManager) -> None:
|
||||
"""Apply backup retention configuration."""
|
||||
if self.days is not None:
|
||||
agents_retention = {
|
||||
agent_id: agent_config.retention
|
||||
for agent_id, agent_config in manager.config.data.agents.items()
|
||||
}
|
||||
|
||||
if self.days is not None or any(
|
||||
agent_retention and agent_retention.days is not None
|
||||
for agent_retention in agents_retention.values()
|
||||
):
|
||||
LOGGER.debug(
|
||||
"Scheduling next automatic delete of backups older than %s in 1 day",
|
||||
self.days,
|
||||
@@ -246,13 +320,6 @@ class RetentionConfig:
|
||||
LOGGER.debug("Unscheduling next automatic delete")
|
||||
self._unschedule_next(manager)
|
||||
|
||||
def to_dict(self) -> StoredRetentionConfig:
|
||||
"""Convert backup retention configuration to a dict."""
|
||||
return StoredRetentionConfig(
|
||||
copies=self.copies,
|
||||
days=self.days,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _schedule_next(
|
||||
self,
|
||||
@@ -271,16 +338,81 @@ class RetentionConfig:
|
||||
"""Return backups older than days to delete."""
|
||||
# we need to check here since we await before
|
||||
# this filter is applied
|
||||
if self.days is None:
|
||||
return {}
|
||||
now = dt_util.utcnow()
|
||||
return {
|
||||
backup_id: backup
|
||||
for backup_id, backup in backups.items()
|
||||
if dt_util.parse_datetime(backup.date, raise_on_error=True)
|
||||
+ timedelta(days=self.days)
|
||||
< now
|
||||
agents_retention = {
|
||||
agent_id: agent_config.retention
|
||||
for agent_id, agent_config in manager.config.data.agents.items()
|
||||
}
|
||||
has_agents_retention = any(
|
||||
agent_retention for agent_retention in agents_retention.values()
|
||||
)
|
||||
has_agents_retention_days = any(
|
||||
agent_retention and agent_retention.days is not None
|
||||
for agent_retention in agents_retention.values()
|
||||
)
|
||||
if (global_days := self.days) is None and not has_agents_retention_days:
|
||||
# No global retention days and no agent retention days
|
||||
return {}
|
||||
|
||||
now = dt_util.utcnow()
|
||||
if global_days is not None and not has_agents_retention:
|
||||
# Return early to avoid the longer filtering below.
|
||||
return {
|
||||
backup_id: backup
|
||||
for backup_id, backup in backups.items()
|
||||
if dt_util.parse_datetime(backup.date, raise_on_error=True)
|
||||
+ timedelta(days=global_days)
|
||||
< now
|
||||
}
|
||||
|
||||
# If there are any agent retention settings, we need to check
|
||||
# the retention settings, for every backup and agent combination.
|
||||
|
||||
backups_to_delete = {}
|
||||
|
||||
for backup_id, backup in backups.items():
|
||||
backup_date = dt_util.parse_datetime(
|
||||
backup.date, raise_on_error=True
|
||||
)
|
||||
delete_from_agents = set(backup.agents)
|
||||
for agent_id in backup.agents:
|
||||
agent_retention = agents_retention.get(agent_id)
|
||||
if agent_retention is None:
|
||||
# This agent does not have a retention setting,
|
||||
# so the global retention setting should be used.
|
||||
if global_days is None:
|
||||
# This agent does not have a retention setting
|
||||
# and the global retention days setting is None,
|
||||
# so this backup should not be deleted.
|
||||
delete_from_agents.discard(agent_id)
|
||||
continue
|
||||
days = global_days
|
||||
elif (agent_days := agent_retention.days) is None:
|
||||
# This agent has a retention setting
|
||||
# where days is set to None,
|
||||
# so the backup should not be deleted.
|
||||
delete_from_agents.discard(agent_id)
|
||||
continue
|
||||
else:
|
||||
# This agent has a retention setting
|
||||
# where days is set to a number,
|
||||
# so that setting should be used.
|
||||
days = agent_days
|
||||
if backup_date + timedelta(days=days) >= now:
|
||||
# This backup is not older than the retention days,
|
||||
# so this agent should not be deleted.
|
||||
delete_from_agents.discard(agent_id)
|
||||
|
||||
filtered_backup = replace(
|
||||
backup,
|
||||
agents={
|
||||
agent_id: agent_backup_status
|
||||
for agent_id, agent_backup_status in backup.agents.items()
|
||||
if agent_id in delete_from_agents
|
||||
},
|
||||
)
|
||||
backups_to_delete[backup_id] = filtered_backup
|
||||
|
||||
return backups_to_delete
|
||||
|
||||
await manager.async_delete_filtered_backups(
|
||||
include_filter=_automatic_backups_filter, delete_filter=_delete_filter
|
||||
@@ -312,6 +444,10 @@ class RetentionParametersDict(TypedDict, total=False):
|
||||
days: int | None
|
||||
|
||||
|
||||
class AgentRetentionConfig(BaseRetentionConfig):
|
||||
"""Represent an agent retention configuration."""
|
||||
|
||||
|
||||
class StoredBackupSchedule(TypedDict):
|
||||
"""Represent the stored backup schedule configuration."""
|
||||
|
||||
@@ -554,16 +690,87 @@ async def delete_backups_exceeding_configured_count(manager: BackupManager) -> N
|
||||
backups: dict[str, ManagerBackup],
|
||||
) -> dict[str, ManagerBackup]:
|
||||
"""Return oldest backups more numerous than copies to delete."""
|
||||
agents_retention = {
|
||||
agent_id: agent_config.retention
|
||||
for agent_id, agent_config in manager.config.data.agents.items()
|
||||
}
|
||||
has_agents_retention = any(
|
||||
agent_retention for agent_retention in agents_retention.values()
|
||||
)
|
||||
has_agents_retention_copies = any(
|
||||
agent_retention and agent_retention.copies is not None
|
||||
for agent_retention in agents_retention.values()
|
||||
)
|
||||
# we need to check here since we await before
|
||||
# this filter is applied
|
||||
if manager.config.data.retention.copies is None:
|
||||
if (
|
||||
global_copies := manager.config.data.retention.copies
|
||||
) is None and not has_agents_retention_copies:
|
||||
# No global retention copies and no agent retention copies
|
||||
return {}
|
||||
return dict(
|
||||
sorted(
|
||||
backups.items(),
|
||||
key=lambda backup_item: backup_item[1].date,
|
||||
)[: max(len(backups) - manager.config.data.retention.copies, 0)]
|
||||
if global_copies is not None and not has_agents_retention:
|
||||
# Return early to avoid the longer filtering below.
|
||||
return dict(
|
||||
sorted(
|
||||
backups.items(),
|
||||
key=lambda backup_item: backup_item[1].date,
|
||||
)[: max(len(backups) - global_copies, 0)]
|
||||
)
|
||||
|
||||
backups_by_agent: dict[str, dict[str, ManagerBackup]] = defaultdict(dict)
|
||||
for backup_id, backup in backups.items():
|
||||
for agent_id in backup.agents:
|
||||
backups_by_agent[agent_id][backup_id] = backup
|
||||
|
||||
backups_to_delete_by_agent: dict[str, dict[str, ManagerBackup]] = defaultdict(
|
||||
dict
|
||||
)
|
||||
for agent_id, agent_backups in backups_by_agent.items():
|
||||
agent_retention = agents_retention.get(agent_id)
|
||||
if agent_retention is None:
|
||||
# This agent does not have a retention setting,
|
||||
# so the global retention setting should be used.
|
||||
if global_copies is None:
|
||||
# This agent does not have a retention setting
|
||||
# and the global retention copies setting is None,
|
||||
# so backups should not be deleted.
|
||||
continue
|
||||
# The global retention setting will be used.
|
||||
copies = global_copies
|
||||
elif (agent_copies := agent_retention.copies) is None:
|
||||
# This agent has a retention setting
|
||||
# where copies is set to None,
|
||||
# so backups should not be deleted.
|
||||
continue
|
||||
else:
|
||||
# This agent retention setting will be used.
|
||||
copies = agent_copies
|
||||
|
||||
backups_to_delete_by_agent[agent_id] = dict(
|
||||
sorted(
|
||||
agent_backups.items(),
|
||||
key=lambda backup_item: backup_item[1].date,
|
||||
)[: max(len(agent_backups) - copies, 0)]
|
||||
)
|
||||
|
||||
backup_ids_to_delete: dict[str, set[str]] = defaultdict(set)
|
||||
for agent_id, to_delete in backups_to_delete_by_agent.items():
|
||||
for backup_id in to_delete:
|
||||
backup_ids_to_delete[backup_id].add(agent_id)
|
||||
backups_to_delete: dict[str, ManagerBackup] = {}
|
||||
for backup_id, agent_ids in backup_ids_to_delete.items():
|
||||
backup = backups[backup_id]
|
||||
# filter the backup to only include the agents that should be deleted
|
||||
filtered_backup = replace(
|
||||
backup,
|
||||
agents={
|
||||
agent_id: agent_backup_status
|
||||
for agent_id, agent_backup_status in backup.agents.items()
|
||||
if agent_id in agent_ids
|
||||
},
|
||||
)
|
||||
backups_to_delete[backup_id] = filtered_backup
|
||||
return backups_to_delete
|
||||
|
||||
await manager.async_delete_filtered_backups(
|
||||
include_filter=_automatic_backups_filter, delete_filter=_delete_filter
|
||||
|
||||
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||
STORE_DELAY_SAVE = 30
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION_MINOR = 5
|
||||
STORAGE_VERSION_MINOR = 6
|
||||
|
||||
|
||||
class StoredBackupData(TypedDict):
|
||||
@@ -72,6 +72,10 @@ class _BackupStore(Store[StoredBackupData]):
|
||||
data["config"]["automatic_backups_configured"] = (
|
||||
data["config"]["create_backup"]["password"] is not None
|
||||
)
|
||||
if old_minor_version < 6:
|
||||
# Version 1.6 adds agent retention settings
|
||||
for agent in data["config"]["agents"]:
|
||||
data["config"]["agents"][agent]["retention"] = None
|
||||
|
||||
# Note: We allow reading data with major version 2.
|
||||
# Reject if major version is higher than 2.
|
||||
|
||||
@@ -346,7 +346,28 @@ async def handle_config_info(
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/config/update",
|
||||
vol.Optional("agents"): vol.Schema({str: {"protected": bool}}),
|
||||
vol.Optional("agents"): vol.Schema(
|
||||
{
|
||||
str: {
|
||||
vol.Optional("protected"): bool,
|
||||
vol.Optional("retention"): vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
# Note: We can't use cv.positive_int because it allows 0 even
|
||||
# though 0 is not positive.
|
||||
vol.Optional("copies"): vol.Any(
|
||||
vol.All(int, vol.Range(min=1)), None
|
||||
),
|
||||
vol.Optional("days"): vol.Any(
|
||||
vol.All(int, vol.Range(min=1)), None
|
||||
),
|
||||
},
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional("automatic_backups_configured"): bool,
|
||||
vol.Optional("create_backup"): vol.Schema(
|
||||
{
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluemaestro-ble==0.2.3"]
|
||||
"requirements": ["bluemaestro-ble==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.27.0",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.39.0"
|
||||
"habluetooth==3.45.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bluetooth_adapters import (
|
||||
from habluetooth import (
|
||||
DiscoveredDeviceAdvertisementData,
|
||||
DiscoveredDeviceAdvertisementDataDict,
|
||||
DiscoveryStorageType,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.1"]
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -93,3 +93,5 @@ STT_ENTITY_UNIQUE_ID = "cloud-speech-to-text"
|
||||
TTS_ENTITY_UNIQUE_ID = "cloud-text-to-speech"
|
||||
|
||||
LOGIN_MFA_TIMEOUT = 60
|
||||
|
||||
VOICE_STYLE_SEPERATOR = "||"
|
||||
|
||||
@@ -18,7 +18,7 @@ from aiohttp import web
|
||||
import attr
|
||||
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
from hass_nabucasa.voice import TTS_VOICES
|
||||
from hass_nabucasa.voice_data import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
@@ -57,6 +57,7 @@ from .const import (
|
||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE,
|
||||
PREF_TTS_DEFAULT_VOICE,
|
||||
REQUEST_TIMEOUT,
|
||||
VOICE_STYLE_SEPERATOR,
|
||||
)
|
||||
from .google_config import CLOUD_GOOGLE
|
||||
from .repairs import async_manage_legacy_subscription_issue
|
||||
@@ -591,10 +592,21 @@ async def websocket_subscription(
|
||||
def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
|
||||
"""Validate language and voice."""
|
||||
language, voice = value
|
||||
style: str | None
|
||||
voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
|
||||
if not style:
|
||||
style = None
|
||||
if language not in TTS_VOICES:
|
||||
raise vol.Invalid(f"Invalid language {language}")
|
||||
if voice not in TTS_VOICES[language]:
|
||||
if voice not in (language_info := TTS_VOICES[language]):
|
||||
raise vol.Invalid(f"Invalid voice {voice} for language {language}")
|
||||
voice_info = language_info[voice]
|
||||
if style and (
|
||||
isinstance(voice_info, str) or style not in voice_info.get("variants", [])
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"Invalid style {style} for voice {voice} in language {language}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
@@ -1012,13 +1024,24 @@ def tts_info(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Fetch available tts info."""
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"languages": [
|
||||
(language, voice)
|
||||
for language, voices in TTS_VOICES.items()
|
||||
for voice in voices
|
||||
]
|
||||
},
|
||||
)
|
||||
result = []
|
||||
for language, voices in TTS_VOICES.items():
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append((language, voice_id, voice_info))
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
result.append((language, voice_id, name))
|
||||
result.extend(
|
||||
[
|
||||
(
|
||||
language,
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
connection.send_result(msg["id"], {"languages": result})
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.94.0"],
|
||||
"requirements": ["hass-nabucasa==0.96.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
|
||||
from hass_nabucasa.voice import MAP_VOICE, AudioOutput, Gender, VoiceError
|
||||
from hass_nabucasa.voice_data import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import (
|
||||
@@ -30,7 +31,13 @@ from homeassistant.setup import async_when_setup
|
||||
|
||||
from .assist_pipeline import async_migrate_cloud_pipeline_engine
|
||||
from .client import CloudClient
|
||||
from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID
|
||||
from .const import (
|
||||
DATA_CLOUD,
|
||||
DATA_PLATFORMS_SETUP,
|
||||
DOMAIN,
|
||||
TTS_ENTITY_UNIQUE_ID,
|
||||
VOICE_STYLE_SEPERATOR,
|
||||
)
|
||||
from .prefs import CloudPreferences
|
||||
|
||||
ATTR_GENDER = "gender"
|
||||
@@ -57,6 +64,7 @@ DEFAULT_VOICES = {
|
||||
"ar-SY": "AmanyNeural",
|
||||
"ar-TN": "ReemNeural",
|
||||
"ar-YE": "MaryamNeural",
|
||||
"as-IN": "PriyomNeural",
|
||||
"az-AZ": "BabekNeural",
|
||||
"bg-BG": "KalinaNeural",
|
||||
"bn-BD": "NabanitaNeural",
|
||||
@@ -126,6 +134,8 @@ DEFAULT_VOICES = {
|
||||
"id-ID": "GadisNeural",
|
||||
"is-IS": "GudrunNeural",
|
||||
"it-IT": "ElsaNeural",
|
||||
"iu-Cans-CA": "SiqiniqNeural",
|
||||
"iu-Latn-CA": "SiqiniqNeural",
|
||||
"ja-JP": "NanamiNeural",
|
||||
"jv-ID": "SitiNeural",
|
||||
"ka-GE": "EkaNeural",
|
||||
@@ -147,6 +157,8 @@ DEFAULT_VOICES = {
|
||||
"ne-NP": "HemkalaNeural",
|
||||
"nl-BE": "DenaNeural",
|
||||
"nl-NL": "ColetteNeural",
|
||||
"or-IN": "SubhasiniNeural",
|
||||
"pa-IN": "OjasNeural",
|
||||
"pl-PL": "AgnieszkaNeural",
|
||||
"ps-AF": "LatifaNeural",
|
||||
"pt-BR": "FranciscaNeural",
|
||||
@@ -158,6 +170,7 @@ DEFAULT_VOICES = {
|
||||
"sl-SI": "PetraNeural",
|
||||
"so-SO": "UbaxNeural",
|
||||
"sq-AL": "AnilaNeural",
|
||||
"sr-Latn-RS": "NicholasNeural",
|
||||
"sr-RS": "SophieNeural",
|
||||
"su-ID": "TutiNeural",
|
||||
"sv-SE": "SofieNeural",
|
||||
@@ -177,12 +190,9 @@ DEFAULT_VOICES = {
|
||||
"vi-VN": "HoaiMyNeural",
|
||||
"wuu-CN": "XiaotongNeural",
|
||||
"yue-CN": "XiaoMinNeural",
|
||||
"zh-CN": "XiaoxiaoNeural",
|
||||
"zh-CN-henan": "YundengNeural",
|
||||
"zh-CN-liaoning": "XiaobeiNeural",
|
||||
"zh-CN-shaanxi": "XiaoniNeural",
|
||||
"zh-CN-shandong": "YunxiangNeural",
|
||||
"zh-CN-sichuan": "YunxiNeural",
|
||||
"zh-CN": "XiaoxiaoNeural",
|
||||
"zh-HK": "HiuMaanNeural",
|
||||
"zh-TW": "HsiaoChenNeural",
|
||||
"zu-ZA": "ThandoNeural",
|
||||
@@ -191,6 +201,39 @@ DEFAULT_VOICES = {
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def _prepare_voice_args(
|
||||
*,
|
||||
hass: HomeAssistant,
|
||||
language: str,
|
||||
voice: str,
|
||||
gender: str | None,
|
||||
) -> dict:
|
||||
"""Prepare voice arguments."""
|
||||
gender = handle_deprecated_gender(hass, gender)
|
||||
style: str | None
|
||||
original_voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
|
||||
if not style:
|
||||
style = None
|
||||
updated_voice = handle_deprecated_voice(hass, original_voice)
|
||||
if updated_voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
updated_voice = default_voice
|
||||
|
||||
return {
|
||||
"language": language,
|
||||
"voice": updated_voice,
|
||||
"gender": gender,
|
||||
"style": style,
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_platform(value: str) -> str:
|
||||
"""Validate if platform is deprecated."""
|
||||
if value == DOMAIN:
|
||||
@@ -328,36 +371,59 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
"""Return a list of supported voices for a language."""
|
||||
if not (voices := TTS_VOICES.get(language)):
|
||||
return None
|
||||
return [Voice(voice, voice) for voice in voices]
|
||||
|
||||
result = []
|
||||
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
voice_info,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
name,
|
||||
)
|
||||
)
|
||||
result.extend(
|
||||
[
|
||||
Voice(
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str = options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice if language == self._language else DEFAULT_VOICES[language],
|
||||
)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
if voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
voice = default_voice
|
||||
# Process TTS
|
||||
try:
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
**_prepare_voice_args(
|
||||
hass=self.hass,
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
)
|
||||
except VoiceError as err:
|
||||
_LOGGER.error("Voice error: %s", err)
|
||||
@@ -401,7 +467,38 @@ class CloudProvider(Provider):
|
||||
"""Return a list of supported voices for a language."""
|
||||
if not (voices := TTS_VOICES.get(language)):
|
||||
return None
|
||||
return [Voice(voice, voice) for voice in voices]
|
||||
|
||||
result = []
|
||||
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
voice_info,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
name,
|
||||
)
|
||||
)
|
||||
result.extend(
|
||||
[
|
||||
Voice(
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, str]:
|
||||
@@ -415,30 +512,22 @@ class CloudProvider(Provider):
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
assert self.hass is not None
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str = options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice if language == self._language else DEFAULT_VOICES[language],
|
||||
)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
if voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
voice = default_voice
|
||||
# Process TTS
|
||||
try:
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
**_prepare_voice_args(
|
||||
hass=self.hass,
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
)
|
||||
except VoiceError as err:
|
||||
_LOGGER.error("Voice error: %s", err)
|
||||
|
||||
@@ -12,6 +12,7 @@ from .coordinator import (
|
||||
ComelitSerialBridge,
|
||||
ComelitVedoSystem,
|
||||
)
|
||||
from .utils import async_client_session
|
||||
|
||||
BRIDGE_PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
@@ -32,6 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
"""Set up Comelit platform."""
|
||||
|
||||
coordinator: ComelitBaseCoordinator
|
||||
|
||||
session = await async_client_session(hass)
|
||||
|
||||
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
coordinator = ComelitSerialBridge(
|
||||
hass,
|
||||
@@ -39,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
entry.data[CONF_PIN],
|
||||
session,
|
||||
)
|
||||
platforms = BRIDGE_PLATFORMS
|
||||
else:
|
||||
@@ -48,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
entry.data[CONF_PIN],
|
||||
session,
|
||||
)
|
||||
platforms = VEDO_PLATFORMS
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
|
||||
from .utils import async_client_session
|
||||
|
||||
DEFAULT_HOST = "192.168.1.252"
|
||||
DEFAULT_PIN = 111111
|
||||
@@ -47,10 +48,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api: ComelitCommonApi
|
||||
|
||||
session = await async_client_session(hass)
|
||||
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
|
||||
api = ComeliteSerialBridgeApi(
|
||||
data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session
|
||||
)
|
||||
else:
|
||||
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
|
||||
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session)
|
||||
|
||||
try:
|
||||
await api.login()
|
||||
|
||||
@@ -15,6 +15,7 @@ from aiocomelit.api import (
|
||||
)
|
||||
from aiocomelit.const import BRIDGE, VEDO
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -95,9 +96,16 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
await self.api.login()
|
||||
return await self._async_update_system_data()
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(repr(err)) from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
) from err
|
||||
|
||||
@abstractmethod
|
||||
async def _async_update_system_data(self) -> T:
|
||||
@@ -119,9 +127,10 @@ class ComelitSerialBridge(
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComeliteSerialBridgeApi(host, port, pin)
|
||||
self.api = ComeliteSerialBridgeApi(host, port, pin, session)
|
||||
super().__init__(hass, entry, BRIDGE, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
@@ -144,9 +153,10 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComelitVedoApi(host, port, pin)
|
||||
self.api = ComelitVedoApi(host, port, pin, session)
|
||||
super().__init__(hass, entry, VEDO, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiocomelit==0.11.3"]
|
||||
"requirements": ["aiocomelit==0.12.0"]
|
||||
}
|
||||
|
||||
@@ -70,9 +70,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: PR in progress
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
@@ -86,7 +84,5 @@ rules:
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: todo
|
||||
comment: implement aiohttp_client.async_create_clientsession
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
||||
@@ -74,7 +74,10 @@
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_authenticate": {
|
||||
"message": "Error authenticating: {error}"
|
||||
"message": "Error authenticating"
|
||||
},
|
||||
"updated_failed": {
|
||||
"message": "Failed to update data: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
homeassistant/components/comelit/utils.py
Normal file
13
homeassistant/components/comelit/utils.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Utils for Comelit."""
|
||||
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
return aiohttp_client.async_create_clientsession(
|
||||
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
|
||||
)
|
||||
@@ -56,7 +56,10 @@ from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
CONF_AVAILABILITY,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -91,7 +94,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(
|
||||
@@ -108,7 +113,9 @@ COVER_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
@@ -134,7 +141,9 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
@@ -150,7 +159,9 @@ SWITCH_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_COMMAND_ON, default="true"): cv.string,
|
||||
vol.Optional(CONF_COMMAND_STATE): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
|
||||
@@ -18,7 +18,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -50,7 +53,7 @@ async def async_setup_platform(
|
||||
scan_interval: timedelta = binary_sensor_config.get(
|
||||
CONF_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
)
|
||||
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template: ValueTemplate | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
@@ -86,7 +89,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
config: ConfigType,
|
||||
payload_on: str,
|
||||
payload_off: str,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the Command line binary sensor."""
|
||||
@@ -133,9 +136,14 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
variables = self._template_variables_with_value(value)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._value_template is not None:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
value, None
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._attr_is_on = None
|
||||
if value == self._payload_on:
|
||||
@@ -143,7 +151,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
elif value == self._payload_off:
|
||||
self._attr_is_on = False
|
||||
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -79,7 +82,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
||||
command_close: str,
|
||||
command_stop: str,
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
@@ -164,14 +167,20 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self._async_query_state())
|
||||
|
||||
variables = self._template_variables_with_value(payload)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._value_template:
|
||||
payload = self._value_template.async_render_with_possible_json_value(
|
||||
payload, None
|
||||
payload = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._state = None
|
||||
if payload:
|
||||
self._state = int(payload)
|
||||
self._process_manual_data(payload)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -23,7 +23,10 @@ from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerSensorEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -57,7 +60,7 @@ async def async_setup_platform(
|
||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
||||
json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
|
||||
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template: ValueTemplate | None = sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
trigger_entity_config = {
|
||||
@@ -88,7 +91,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
config: ConfigType,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
json_attributes: list[str] | None,
|
||||
json_attributes_path: str | None,
|
||||
scan_interval: timedelta,
|
||||
@@ -144,6 +147,11 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
variables = self._template_variables_with_value(self.data.value)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._json_attributes:
|
||||
self._attr_extra_state_attributes = {}
|
||||
if value:
|
||||
@@ -168,16 +176,17 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
else:
|
||||
LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
|
||||
if self._value_template is None:
|
||||
self._attr_native_value = None
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_native_value = None
|
||||
if self._value_template is not None and value is not None:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
value,
|
||||
None,
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
|
||||
if self.device_class not in {
|
||||
@@ -190,7 +199,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -19,7 +19,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -78,7 +81,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
command_on: str,
|
||||
command_off: str,
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
@@ -166,15 +169,21 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self._async_query_state())
|
||||
|
||||
variables = self._template_variables_with_value(payload)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
value = None
|
||||
if self._value_template:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
payload, None
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._attr_is_on = None
|
||||
if payload or value:
|
||||
self._attr_is_on = (value or payload).lower() == "true"
|
||||
self._process_manual_data(payload)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"remote_moved_any_side": "Device moved with any side up",
|
||||
"remote_double_tap_any_side": "Device double tapped on any side",
|
||||
"remote_turned_clockwise": "Device turned clockwise",
|
||||
"remote_turned_counter_clockwise": "Device turned counter clockwise",
|
||||
"remote_turned_counter_clockwise": "Device turned counterclockwise",
|
||||
"remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"",
|
||||
"remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"",
|
||||
"remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"",
|
||||
|
||||
@@ -218,7 +218,7 @@ class TrackerEntity(
|
||||
|
||||
entity_description: TrackerEntityDescription
|
||||
_attr_latitude: float | None = None
|
||||
_attr_location_accuracy: int = 0
|
||||
_attr_location_accuracy: float = 0
|
||||
_attr_location_name: str | None = None
|
||||
_attr_longitude: float | None = None
|
||||
_attr_source_type: SourceType = SourceType.GPS
|
||||
@@ -234,7 +234,7 @@ class TrackerEntity(
|
||||
return not self.should_poll
|
||||
|
||||
@cached_property
|
||||
def location_accuracy(self) -> int:
|
||||
def location_accuracy(self) -> float:
|
||||
"""Return the location accuracy of the device.
|
||||
|
||||
Value in meters.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from devolo_home_control_api.exceptions.gateway import GatewayOfflineError
|
||||
@@ -97,7 +97,7 @@ async def async_remove_config_entry_device(
|
||||
return True
|
||||
|
||||
|
||||
def configure_mydevolo(conf: dict[str, Any] | MappingProxyType[str, Any]) -> Mydevolo:
|
||||
def configure_mydevolo(conf: Mapping[str, Any]) -> Mydevolo:
|
||||
"""Configure mydevolo."""
|
||||
mydevolo = Mydevolo()
|
||||
mydevolo.user = conf[CONF_USERNAME]
|
||||
|
||||
@@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
SENSOR_TYPES[CONNECTED_PLC_DEVICES],
|
||||
)
|
||||
)
|
||||
network = await device.plcnet.async_get_network_overview()
|
||||
network: LogicalNetwork = coordinators[CONNECTED_PLC_DEVICES].data
|
||||
peers = [
|
||||
peer.mac_address for peer in network.devices if peer.topology == REMOTE
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Dialogflow Webhook",
|
||||
"title": "Set up the Dialogflow webhook",
|
||||
"description": "Are you sure you want to set up Dialogflow?"
|
||||
}
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to set up [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
|
||||
"default": "To send events to Home Assistant, you will need to set up the [webhook service of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from types import MappingProxyType
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any
|
||||
|
||||
from dynalite_devices_lib.dynalite_devices import (
|
||||
@@ -50,7 +49,7 @@ class DynaliteBridge:
|
||||
LOGGER.debug("Setting up bridge - host %s", self.host)
|
||||
return await self.dynalite_devices.async_setup()
|
||||
|
||||
def reload_config(self, config: MappingProxyType[str, Any]) -> None:
|
||||
def reload_config(self, config: Mapping[str, Any]) -> None:
|
||||
"""Reconfigure a bridge when config changes."""
|
||||
LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config)
|
||||
self.dynalite_devices.configure(convert_config(config))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import MappingProxyType
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from dynalite_devices_lib import const as dyn_const
|
||||
@@ -138,9 +138,7 @@ def convert_template(config: dict[str, Any]) -> dict[str, Any]:
|
||||
return convert_with_map(config, my_map)
|
||||
|
||||
|
||||
def convert_config(
|
||||
config: dict[str, Any] | MappingProxyType[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
def convert_config(config: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Convert a config dict by replacing component consts with library consts."""
|
||||
my_map = {
|
||||
CONF_NAME: dyn_const.CONF_NAME,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from elevenlabs import AsyncElevenLabs
|
||||
@@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def to_voice_settings(options: MappingProxyType[str, Any]) -> VoiceSettings:
|
||||
def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings:
|
||||
"""Return voice settings."""
|
||||
return VoiceSettings(
|
||||
stability=options.get(CONF_STABILITY, DEFAULT_STABILITY),
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
@@ -235,7 +234,7 @@ def _async_find_matching_config_entry(
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
||||
"""Set up Elk-M1 Control from a config entry."""
|
||||
conf: MappingProxyType[str, Any] = entry.data
|
||||
conf = entry.data
|
||||
|
||||
host = hostname_from_url(entry.data[CONF_HOST])
|
||||
|
||||
|
||||
@@ -293,9 +293,9 @@ async def ws_get_fossil_energy_consumption(
|
||||
if statistics_id not in statistic_ids:
|
||||
continue
|
||||
for period in stat:
|
||||
if period["change"] is None:
|
||||
if (change := period.get("change")) is None:
|
||||
continue
|
||||
result[period["start"]] += period["change"]
|
||||
result[period["start"]] += change
|
||||
|
||||
return {key: result[key] for key in sorted(result)}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from pyenphase import Envoy, EnvoyError, EnvoyTokenAuth
|
||||
from pyenphase.models.home import EnvoyInterfaceInformation
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -26,7 +28,7 @@ TOKEN_REFRESH_CHECK_INTERVAL = timedelta(days=1)
|
||||
STALE_TOKEN_THRESHOLD = timedelta(days=30).total_seconds()
|
||||
NOTIFICATION_ID = "enphase_envoy_notification"
|
||||
FIRMWARE_REFRESH_INTERVAL = timedelta(hours=4)
|
||||
|
||||
MAC_VERIFICATION_DELAY = timedelta(seconds=34)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -39,6 +41,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
envoy_serial_number: str
|
||||
envoy_firmware: str
|
||||
config_entry: EnphaseConfigEntry
|
||||
interface: EnvoyInterfaceInformation | None
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, envoy: Envoy, entry: EnphaseConfigEntry
|
||||
@@ -50,8 +53,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.password = entry_data[CONF_PASSWORD]
|
||||
self._setup_complete = False
|
||||
self.envoy_firmware = ""
|
||||
self.interface = None
|
||||
self._cancel_token_refresh: CALLBACK_TYPE | None = None
|
||||
self._cancel_firmware_refresh: CALLBACK_TYPE | None = None
|
||||
self._cancel_mac_verification: CALLBACK_TYPE | None = None
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@@ -121,6 +126,66 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
def _schedule_mac_verification(
|
||||
self, delay: timedelta = MAC_VERIFICATION_DELAY
|
||||
) -> None:
|
||||
"""Schedule one time job to verify envoy mac address."""
|
||||
self.async_cancel_mac_verification()
|
||||
self._cancel_mac_verification = async_call_later(
|
||||
self.hass,
|
||||
delay,
|
||||
self._async_verify_mac,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_verify_mac(self, now: datetime.datetime) -> None:
|
||||
"""Verify Envoy active interface mac address in background."""
|
||||
self.hass.async_create_background_task(
|
||||
self._async_fetch_and_compare_mac(), "{name} verify envoy mac address"
|
||||
)
|
||||
|
||||
async def _async_fetch_and_compare_mac(self) -> None:
|
||||
"""Get Envoy interface information and update mac in device connections."""
|
||||
interface: (
|
||||
EnvoyInterfaceInformation | None
|
||||
) = await self.envoy.interface_settings()
|
||||
if interface is None:
|
||||
_LOGGER.debug("%s: interface information returned None", self.name)
|
||||
return
|
||||
# remember interface information so diagnostics can include in report
|
||||
self.interface = interface
|
||||
|
||||
# Add to or update device registry connections as needed
|
||||
device_registry = dr.async_get(self.hass)
|
||||
envoy_device = device_registry.async_get_device(
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
}
|
||||
)
|
||||
if envoy_device is None:
|
||||
_LOGGER.error(
|
||||
"No envoy device found in device registry: %s %s",
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
return
|
||||
|
||||
connection = (dr.CONNECTION_NETWORK_MAC, interface.mac)
|
||||
if connection in envoy_device.connections:
|
||||
_LOGGER.debug(
|
||||
"connection verified as existing: %s in %s", connection, self.name
|
||||
)
|
||||
return
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id=envoy_device.id,
|
||||
new_connections={connection},
|
||||
)
|
||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||
|
||||
@callback
|
||||
def _async_mark_setup_complete(self) -> None:
|
||||
"""Mark setup as complete and setup firmware checks and token refresh if needed."""
|
||||
@@ -132,6 +197,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
FIRMWARE_REFRESH_INTERVAL,
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
self._schedule_mac_verification()
|
||||
self.async_cancel_token_refresh()
|
||||
if not isinstance(self.envoy.auth, EnvoyTokenAuth):
|
||||
return
|
||||
@@ -252,3 +318,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if self._cancel_firmware_refresh:
|
||||
self._cancel_firmware_refresh()
|
||||
self._cancel_firmware_refresh = None
|
||||
|
||||
@callback
|
||||
def async_cancel_mac_verification(self) -> None:
|
||||
"""Cancel mac verification."""
|
||||
if self._cancel_mac_verification:
|
||||
self._cancel_mac_verification()
|
||||
self._cancel_mac_verification = None
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from attr import asdict
|
||||
@@ -63,6 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/ensemble/generator",
|
||||
"/ivp/meters",
|
||||
"/ivp/meters/readings",
|
||||
"/home,",
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
@@ -146,11 +148,25 @@ async def async_get_config_entry_diagnostics(
|
||||
"inverters": envoy_data.inverters,
|
||||
"tariff": envoy_data.tariff,
|
||||
}
|
||||
# Add Envoy active interface information to report
|
||||
active_interface: dict[str, Any] = {}
|
||||
if coordinator.interface:
|
||||
active_interface = {
|
||||
"name": (interface := coordinator.interface).primary_interface,
|
||||
"interface type": interface.interface_type,
|
||||
"mac": interface.mac,
|
||||
"uses dhcp": interface.dhcp,
|
||||
"firmware build date": datetime.fromtimestamp(
|
||||
interface.software_build_epoch
|
||||
).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"envoy timezone": interface.timezone,
|
||||
}
|
||||
|
||||
envoy_properties: dict[str, Any] = {
|
||||
"envoy_firmware": envoy.firmware,
|
||||
"part_number": envoy.part_number,
|
||||
"envoy_model": envoy.envoy_model,
|
||||
"active interface": active_interface,
|
||||
"supported_features": [feature.name for feature in envoy.supported_features],
|
||||
"phase_mode": envoy.phase_mode,
|
||||
"phase_count": envoy.phase_count,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==1.26.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: done
|
||||
status: exempt
|
||||
comment: only actions implemented are platform native ones.
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: fixed 1 minute cycle based on Enphase Envoy device characteristics
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy/#actions
|
||||
docs-high-level-description:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy
|
||||
docs-installation-instructions:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#prerequisites
|
||||
docs-removal-instructions:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#removing-the-integration
|
||||
entity-event-setup:
|
||||
status: done
|
||||
comment: no events used.
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
@@ -34,24 +22,14 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: todo
|
||||
comment: |
|
||||
needs to raise appropriate error when exception occurs.
|
||||
Pending https://github.com/pyenphase/pyenphase/pull/194
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#configuration
|
||||
docs-installation-parameters:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#required-manual-input
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates:
|
||||
status: done
|
||||
comment: pending https://github.com/home-assistant/core/pull/132373
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
@@ -60,22 +38,14 @@ rules:
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#data-updates
|
||||
docs-examples:
|
||||
status: todo
|
||||
comment: add blue-print examples, if any
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#supported-devices
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting:
|
||||
status: done
|
||||
comment: https://www.home-assistant.io/integrations/enphase_envoy#troubleshooting
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
@@ -86,7 +56,7 @@ rules:
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: no general issues or repair.py
|
||||
stale-devices: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"storage_mode": {
|
||||
"name": "Storage mode",
|
||||
"state": {
|
||||
"self_consumption": "Self consumption",
|
||||
"self_consumption": "Self-consumption",
|
||||
"backup": "Full backup",
|
||||
"savings": "Savings mode"
|
||||
}
|
||||
@@ -393,7 +393,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"unexpected_device": {
|
||||
"message": "Unexpected Envoy serial-number found at {host}; expected {expected_serial}, found {actual_serial}"
|
||||
"message": "Unexpected Envoy serial number found at {host}; expected {expected_serial}, found {actual_serial}"
|
||||
},
|
||||
"authentication_error": {
|
||||
"message": "Envoy authentication failure on {host}: {args}"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.10.1"]
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"name": "AQHI"
|
||||
},
|
||||
"advisories": {
|
||||
"name": "Advisory"
|
||||
"name": "Advisories"
|
||||
},
|
||||
"endings": {
|
||||
"name": "Endings"
|
||||
|
||||
@@ -94,6 +94,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
self._ember = ember
|
||||
self._zone_name = zone_name(zone)
|
||||
self._zone = zone
|
||||
self._attr_unique_id = zone["zoneid"]
|
||||
|
||||
# hot water = true, is immersive device without target temperature control.
|
||||
self._hot_water = zone_is_hotwater(zone)
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> bool:
|
||||
"""Unload an esphome config entry."""
|
||||
entry_data = await cleanup_instance(hass, entry)
|
||||
entry_data = await cleanup_instance(entry)
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
entry, entry_data.loaded_platforms
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[
|
||||
|
||||
|
||||
class EspHomeACPFeatures(APIIntEnum):
|
||||
"""ESPHome AlarmCintolPanel feature numbers."""
|
||||
"""ESPHome AlarmControlPanel feature numbers."""
|
||||
|
||||
ARM_HOME = 1
|
||||
ARM_AWAY = 2
|
||||
|
||||
@@ -35,15 +35,14 @@ from homeassistant.components.intent import (
|
||||
async_register_timer_handler,
|
||||
)
|
||||
from homeassistant.components.media_player import async_process_play_media_url
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EsphomeAssistEntity
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .entity import EsphomeAssistEntity, convert_api_error_ha_error
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
from .ffmpeg_proxy import async_create_proxy_url
|
||||
|
||||
@@ -97,7 +96,7 @@ async def async_setup_entry(
|
||||
if entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
async_add_entities([EsphomeAssistSatellite(entry, entry_data)])
|
||||
async_add_entities([EsphomeAssistSatellite(entry)])
|
||||
|
||||
|
||||
class EsphomeAssistSatellite(
|
||||
@@ -109,17 +108,12 @@ class EsphomeAssistSatellite(
|
||||
key="assist_satellite", translation_key="assist_satellite"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
entry_data: RuntimeEntryData,
|
||||
) -> None:
|
||||
def __init__(self, entry: ESPHomeConfigEntry) -> None:
|
||||
"""Initialize satellite."""
|
||||
super().__init__(entry_data)
|
||||
super().__init__(entry.runtime_data)
|
||||
|
||||
self.config_entry = config_entry
|
||||
self.entry_data = entry_data
|
||||
self.cli = self.entry_data.client
|
||||
self.config_entry = entry
|
||||
self.cli = self._entry_data.client
|
||||
|
||||
self._is_running: bool = True
|
||||
self._pipeline_task: asyncio.Task | None = None
|
||||
@@ -135,23 +129,23 @@ class EsphomeAssistSatellite(
|
||||
@property
|
||||
def pipeline_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the pipeline to use for the next conversation."""
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
return ent_reg.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{self.entry_data.device_info.mac_address}-pipeline",
|
||||
f"{self._entry_data.device_info.mac_address}-pipeline",
|
||||
)
|
||||
|
||||
@property
|
||||
def vad_sensitivity_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the VAD sensitivity to use for the next conversation."""
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
return ent_reg.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{self.entry_data.device_info.mac_address}-vad_sensitivity",
|
||||
f"{self._entry_data.device_info.mac_address}-vad_sensitivity",
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -197,16 +191,16 @@ class EsphomeAssistSatellite(
|
||||
_LOGGER.debug("Received satellite configuration: %s", self._satellite_config)
|
||||
|
||||
# Inform listeners that config has been updated
|
||||
self.entry_data.async_assist_satellite_config_updated(self._satellite_config)
|
||||
self._entry_data.async_assist_satellite_config_updated(self._satellite_config)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.API_AUDIO:
|
||||
@@ -262,7 +256,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# Update wake word select when config is updated
|
||||
self.async_on_remove(
|
||||
self.entry_data.async_register_assist_satellite_set_wake_word_callback(
|
||||
self._entry_data.async_register_assist_satellite_set_wake_word_callback(
|
||||
self.async_set_wake_word
|
||||
)
|
||||
)
|
||||
@@ -284,7 +278,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
data_to_send: dict[str, Any] = {}
|
||||
if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_START:
|
||||
self.entry_data.async_set_assist_pipeline_state(True)
|
||||
self._entry_data.async_set_assist_pipeline_state(True)
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
@@ -306,10 +300,10 @@ class EsphomeAssistSatellite(
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER and (
|
||||
@@ -336,13 +330,20 @@ class EsphomeAssistSatellite(
|
||||
"code": event.data["code"],
|
||||
"message": event.data["message"],
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
|
||||
assert event.data is not None
|
||||
if tts_output := event.data["tts_output"]:
|
||||
path = tts_output["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
||||
if self._tts_streaming_task is None:
|
||||
# No TTS
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
self._entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
self.cli.send_voice_assistant_event(event_type, data_to_send)
|
||||
|
||||
@convert_api_error_ha_error
|
||||
async def async_announce(
|
||||
self, announcement: assist_satellite.AssistSatelliteAnnouncement
|
||||
) -> None:
|
||||
@@ -352,6 +353,7 @@ class EsphomeAssistSatellite(
|
||||
"""
|
||||
await self._do_announce(announcement, run_pipeline_after=False)
|
||||
|
||||
@convert_api_error_ha_error
|
||||
async def async_start_conversation(
|
||||
self, start_announcement: assist_satellite.AssistSatelliteAnnouncement
|
||||
) -> None:
|
||||
@@ -379,7 +381,7 @@ class EsphomeAssistSatellite(
|
||||
# Route media through the proxy
|
||||
format_to_use: MediaPlayerSupportedFormat | None = None
|
||||
for supported_format in chain(
|
||||
*self.entry_data.media_player_formats.values()
|
||||
*self._entry_data.media_player_formats.values()
|
||||
):
|
||||
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
|
||||
format_to_use = supported_format
|
||||
@@ -437,10 +439,10 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# API or UDP output audio
|
||||
port: int = 0
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if (feature_flags & VoiceAssistantFeature.SPEAKER) and not (
|
||||
@@ -541,7 +543,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
def _update_tts_format(self) -> None:
|
||||
"""Update the TTS format from the first media player."""
|
||||
for supported_format in chain(*self.entry_data.media_player_formats.values()):
|
||||
for supported_format in chain(*self._entry_data.media_player_formats.values()):
|
||||
# Find first announcement format
|
||||
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
|
||||
self._attr_tts_options = {
|
||||
@@ -627,7 +629,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# State change
|
||||
self.tts_response_finished()
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
self._entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
async def _wrap_audio_stream(self) -> AsyncIterable[bytes]:
|
||||
"""Yield audio chunks from the queue until None."""
|
||||
|
||||
@@ -2,50 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from functools import partial
|
||||
|
||||
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .entity import EsphomeEntity, platform_async_setup_entry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up ESPHome binary sensors based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
hass,
|
||||
entry,
|
||||
async_add_entities,
|
||||
info_type=BinarySensorInfo,
|
||||
entity_type=EsphomeBinarySensor,
|
||||
state_type=BinarySensorState,
|
||||
)
|
||||
|
||||
entry_data = entry.runtime_data
|
||||
assert entry_data.device_info is not None
|
||||
if entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)])
|
||||
|
||||
|
||||
class EsphomeBinarySensor(
|
||||
EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity
|
||||
):
|
||||
@@ -76,50 +48,9 @@ class EsphomeBinarySensor(
|
||||
return self._static_info.is_status_binary_sensor or super().available
|
||||
|
||||
|
||||
class EsphomeAssistInProgressBinarySensor(EsphomeAssistEntity, BinarySensorEntity):
|
||||
"""A binary sensor implementation for ESPHome for use with assist_pipeline."""
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
key="assist_in_progress",
|
||||
translation_key="assist_in_progress",
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Create issue."""
|
||||
await super().async_added_to_hass()
|
||||
if TYPE_CHECKING:
|
||||
assert self.registry_entry is not None
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"assist_in_progress_deprecated_{self.registry_entry.id}",
|
||||
breaks_in_ha_version="2025.4",
|
||||
data={
|
||||
"entity_id": self.entity_id,
|
||||
"entity_uuid": self.registry_entry.id,
|
||||
"integration_name": "ESPHome",
|
||||
},
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="assist_in_progress_deprecated",
|
||||
translation_placeholders={
|
||||
"integration_name": "ESPHome",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove issue."""
|
||||
await super().async_will_remove_from_hass()
|
||||
if TYPE_CHECKING:
|
||||
assert self.registry_entry is not None
|
||||
ir.async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"assist_in_progress_deprecated_{self.registry_entry.id}",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._entry_data.assist_pipeline_state
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
info_type=BinarySensorInfo,
|
||||
entity_type=EsphomeBinarySensor,
|
||||
state_type=BinarySensorState,
|
||||
)
|
||||
|
||||
@@ -180,13 +180,13 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
|
||||
def _get_precision(self) -> float:
|
||||
"""Return the precision of the climate device."""
|
||||
precicions = [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
|
||||
precisions = [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
|
||||
static_info = self._static_info
|
||||
if static_info.visual_current_temperature_step != 0:
|
||||
step = static_info.visual_current_temperature_step
|
||||
else:
|
||||
step = static_info.visual_target_temperature_step
|
||||
for prec in precicions:
|
||||
for prec in precisions:
|
||||
if step >= prec:
|
||||
return prec
|
||||
# Fall back to highest precision, tenths
|
||||
|
||||
@@ -177,7 +177,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by a reconfig request."""
|
||||
self._reconfig_entry = self._get_reconfigure_entry()
|
||||
@@ -323,7 +323,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
):
|
||||
return
|
||||
assert conflict_entry.unique_id is not None
|
||||
if updates:
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
error = "reconfigure_already_configured"
|
||||
elif updates:
|
||||
error = "already_configured_updates"
|
||||
else:
|
||||
error = "already_configured_detailed"
|
||||
@@ -662,10 +664,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return ERROR_REQUIRES_ENCRYPTION_KEY
|
||||
except InvalidEncryptionKeyAPIError as ex:
|
||||
if ex.received_name:
|
||||
device_name_changed = self._device_name != ex.received_name
|
||||
self._device_name = ex.received_name
|
||||
if ex.received_mac:
|
||||
self._device_mac = format_mac(ex.received_mac)
|
||||
self._name = ex.received_name
|
||||
if not self._name or device_name_changed:
|
||||
self._name = ex.received_name
|
||||
return ERROR_INVALID_ENCRYPTION_KEY
|
||||
except ResolveAPIError:
|
||||
return "resolve_error"
|
||||
|
||||
@@ -5,43 +5,38 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from awesomeversion import AwesomeVersion
|
||||
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
|
||||
REFRESH_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
|
||||
"""Class to interact with the ESPHome dashboard."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
addon_slug: str,
|
||||
url: str,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
def __init__(self, hass: HomeAssistant, addon_slug: str, url: str) -> None:
|
||||
"""Initialize the dashboard coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name="ESPHome Dashboard",
|
||||
update_interval=timedelta(minutes=5),
|
||||
update_interval=REFRESH_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.addon_slug = addon_slug
|
||||
self.url = url
|
||||
self.api = ESPHomeDashboardAPI(url, session)
|
||||
self.api = ESPHomeDashboardAPI(url, async_get_clientsession(hass))
|
||||
self.supports_update: bool | None = None
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
async def _async_update_data(self) -> dict[str, ConfiguredDevice]:
|
||||
"""Fetch device data."""
|
||||
devices = await self.api.get_devices()
|
||||
configured_devices = devices["configured"]
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -104,9 +103,7 @@ class ESPHomeDashboardManager:
|
||||
self._cancel_shutdown = None
|
||||
self._current_dashboard = None
|
||||
|
||||
dashboard = ESPHomeDashboardCoordinator(
|
||||
hass, addon_slug, url, async_get_clientsession(hass)
|
||||
)
|
||||
dashboard = ESPHomeDashboardCoordinator(hass, addon_slug, url)
|
||||
await dashboard.async_request_refresh()
|
||||
|
||||
self._current_dashboard = dashboard
|
||||
|
||||
@@ -17,15 +17,12 @@ STORAGE_VERSION = 1
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DomainData:
|
||||
"""Define a class that stores global esphome data in hass.data[DOMAIN]."""
|
||||
"""Define a class that stores global esphome data."""
|
||||
|
||||
_stores: dict[str, ESPHomeStorage] = field(default_factory=dict)
|
||||
|
||||
def get_entry_data(self, entry: ESPHomeConfigEntry) -> RuntimeEntryData:
|
||||
"""Return the runtime entry data associated with this config entry.
|
||||
|
||||
Raises KeyError if the entry isn't loaded yet.
|
||||
"""
|
||||
"""Return the runtime entry data associated with this config entry."""
|
||||
return entry.runtime_data
|
||||
|
||||
def get_or_create_store(
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar, cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EntityCategory as EsphomeEntityCategory,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
@@ -155,7 +156,7 @@ def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
|
||||
return _wrapper
|
||||
|
||||
|
||||
def convert_api_error_ha_error[**_P, _R, _EntityT: EsphomeEntity[Any, Any]](
|
||||
def convert_api_error_ha_error[**_P, _R, _EntityT: EsphomeBaseEntity](
|
||||
func: Callable[Concatenate[_EntityT, _P], Awaitable[None]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate ESPHome command calls that send commands/make changes to the device.
|
||||
@@ -194,15 +195,21 @@ ENTITY_CATEGORIES: EsphomeEnumMapper[EsphomeEntityCategory, EntityCategory | Non
|
||||
)
|
||||
|
||||
|
||||
class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
class EsphomeBaseEntity(Entity):
|
||||
"""Define a base esphome entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_device_info: EsphomeDeviceInfo
|
||||
device_entry: dr.DeviceEntry
|
||||
|
||||
|
||||
class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
"""Define an esphome entity."""
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
device_entry: dr.DeviceEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -325,15 +332,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class EsphomeAssistEntity(Entity):
|
||||
class EsphomeAssistEntity(EsphomeBaseEntity):
|
||||
"""Define a base entity for Assist Pipeline entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, entry_data: RuntimeEntryData) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
self._entry_data: RuntimeEntryData = entry_data
|
||||
self._entry_data = entry_data
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
|
||||
@@ -106,7 +106,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the entity is on."""
|
||||
return self._state.state
|
||||
|
||||
@@ -126,7 +126,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def oscillating(self) -> bool | None:
|
||||
def oscillating(self) -> bool:
|
||||
"""Return the oscillation state."""
|
||||
return self._state.oscillating
|
||||
|
||||
@@ -138,7 +138,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def preset_mode(self) -> str | None:
|
||||
def preset_mode(self) -> str:
|
||||
"""Return the current fan preset mode."""
|
||||
return self._state.preset_mode
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache, partial
|
||||
from operator import methodcaller
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -108,7 +109,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
|
||||
def _color_mode_to_ha(mode: int) -> str:
|
||||
"""Convert an esphome color mode to a HA color mode constant.
|
||||
|
||||
Choses the color mode that best matches the feature-set.
|
||||
Choose the color mode that best matches the feature-set.
|
||||
"""
|
||||
candidates = []
|
||||
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
|
||||
@@ -148,7 +149,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
|
||||
# popcount with bin() function because it appears
|
||||
# to be the best way: https://stackoverflow.com/a/9831671
|
||||
color_modes_list = list(color_modes)
|
||||
color_modes_list.sort(key=lambda mode: (mode).bit_count())
|
||||
color_modes_list.sort(key=methodcaller("bit_count"))
|
||||
return color_modes_list[0]
|
||||
|
||||
|
||||
@@ -160,7 +161,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the light is on."""
|
||||
return self._state.state
|
||||
|
||||
@@ -292,13 +293,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def brightness(self) -> int | None:
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return round(self._state.brightness * 255)
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def color_mode(self) -> str | None:
|
||||
def color_mode(self) -> str:
|
||||
"""Return the color mode of the light."""
|
||||
if not self._supports_color_mode:
|
||||
supported_color_modes = self.supported_color_modes
|
||||
@@ -310,7 +311,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
def rgb_color(self) -> tuple[int, int, int]:
|
||||
"""Return the rgb color value [int, int, int]."""
|
||||
state = self._state
|
||||
if not self._supports_color_mode:
|
||||
@@ -328,7 +329,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def rgbw_color(self) -> tuple[int, int, int, int] | None:
|
||||
def rgbw_color(self) -> tuple[int, int, int, int]:
|
||||
"""Return the rgbw color value [int, int, int, int]."""
|
||||
white = round(self._state.white * 255)
|
||||
rgb = cast("tuple[int, int, int]", self.rgb_color)
|
||||
@@ -336,7 +337,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
|
||||
def rgbww_color(self) -> tuple[int, int, int, int, int]:
|
||||
"""Return the rgbww color value [int, int, int, int, int]."""
|
||||
state = self._state
|
||||
rgb = cast("tuple[int, int, int]", self.rgb_color)
|
||||
@@ -372,7 +373,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def effect(self) -> str | None:
|
||||
def effect(self) -> str:
|
||||
"""Return the current effect."""
|
||||
return self._state.effect
|
||||
|
||||
|
||||
@@ -40,25 +40,25 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_locked(self) -> bool | None:
|
||||
def is_locked(self) -> bool:
|
||||
"""Return true if the lock is locked."""
|
||||
return self._state.state is LockState.LOCKED
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_locking(self) -> bool | None:
|
||||
def is_locking(self) -> bool:
|
||||
"""Return true if the lock is locking."""
|
||||
return self._state.state is LockState.LOCKING
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_unlocking(self) -> bool | None:
|
||||
def is_unlocking(self) -> bool:
|
||||
"""Return true if the lock is unlocking."""
|
||||
return self._state.state is LockState.UNLOCKING
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_jammed(self) -> bool | None:
|
||||
def is_jammed(self) -> bool:
|
||||
"""Return true if the lock is jammed (incomplete locking)."""
|
||||
return self._state.state is LockState.JAMMED
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
@@ -215,7 +216,7 @@ class ESPHomeManager:
|
||||
|
||||
async def on_stop(self, event: Event) -> None:
|
||||
"""Cleanup the socket client on HA close."""
|
||||
await cleanup_instance(self.hass, self.entry)
|
||||
await cleanup_instance(self.entry)
|
||||
|
||||
@property
|
||||
def services_issue(self) -> str:
|
||||
@@ -376,7 +377,7 @@ class ESPHomeManager:
|
||||
async def on_connect(self) -> None:
|
||||
"""Subscribe to states and list entities on successful API login."""
|
||||
try:
|
||||
await self._on_connnect()
|
||||
await self._on_connect()
|
||||
except APIConnectionError as err:
|
||||
_LOGGER.warning(
|
||||
"Error getting setting up connection for %s: %s", self.host, err
|
||||
@@ -412,7 +413,7 @@ class ESPHomeManager:
|
||||
self._async_on_log, self._log_level
|
||||
)
|
||||
|
||||
async def _on_connnect(self) -> None:
|
||||
async def _on_connect(self) -> None:
|
||||
"""Subscribe to states and list entities on successful API login."""
|
||||
entry = self.entry
|
||||
unique_id = entry.unique_id
|
||||
@@ -654,6 +655,30 @@ class ESPHomeManager:
|
||||
):
|
||||
self._async_subscribe_logs(new_log_level)
|
||||
|
||||
@callback
|
||||
def _async_cleanup(self) -> None:
|
||||
"""Cleanup stale issues and entities."""
|
||||
assert self.entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
# Cleanup stale assist_in_progress entity and issue,
|
||||
# Remove this after 2026.4
|
||||
if not (
|
||||
stale_entry_entity_id := ent_reg.async_get_entity_id(
|
||||
DOMAIN,
|
||||
Platform.BINARY_SENSOR,
|
||||
f"{self.entry_data.device_info.mac_address}-assist_in_progress",
|
||||
)
|
||||
):
|
||||
return
|
||||
stale_entry = ent_reg.async_get(stale_entry_entity_id)
|
||||
assert stale_entry is not None
|
||||
ent_reg.async_remove(stale_entry_entity_id)
|
||||
issue_reg = ir.async_get(self.hass)
|
||||
if issue := issue_reg.async_get_issue(
|
||||
DOMAIN, f"assist_in_progress_deprecated_{stale_entry.id}"
|
||||
):
|
||||
issue_reg.async_delete(DOMAIN, issue.issue_id)
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start the esphome connection manager."""
|
||||
hass = self.hass
|
||||
@@ -696,6 +721,7 @@ class ESPHomeManager:
|
||||
_setup_services(hass, entry_data, services)
|
||||
|
||||
if (device_info := entry_data.device_info) is not None:
|
||||
self._async_cleanup()
|
||||
if device_info.name:
|
||||
reconnect_logic.name = device_info.name
|
||||
if (
|
||||
@@ -939,9 +965,7 @@ def _setup_services(
|
||||
_async_register_service(hass, entry_data, device_info, service)
|
||||
|
||||
|
||||
async def cleanup_instance(
|
||||
hass: HomeAssistant, entry: ESPHomeConfigEntry
|
||||
) -> RuntimeEntryData:
|
||||
async def cleanup_instance(entry: ESPHomeConfigEntry) -> RuntimeEntryData:
|
||||
"""Cleanup the esphome client if it exists."""
|
||||
data = entry.runtime_data
|
||||
data.async_on_disconnect()
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==30.0.1",
|
||||
"aioesphomeapi==30.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.13.1"
|
||||
"bleak-esphome==2.14.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class EsphomeMediaPlayer(
|
||||
|
||||
@property
|
||||
@esphome_float_state_property
|
||||
def volume_level(self) -> float | None:
|
||||
def volume_level(self) -> float:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._state.volume
|
||||
|
||||
|
||||
85
homeassistant/components/esphome/quality_scale.yaml
Normal file
85
homeassistant/components/esphome/quality_scale.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since actions are defined per device, rather than per integration,
|
||||
they are specific to the device's YAML configuration. Additionally,
|
||||
ESPHome allows for user-defined actions, making it impossible to
|
||||
set them up until the device is connected as they vary by device. For more
|
||||
information, see: https://esphome.io/components/api.html#user-defined-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: |
|
||||
Since actions are defined per device, rather than per integration,
|
||||
they are specific to the device's YAML configuration. Additionally,
|
||||
ESPHome allows for user-defined actions, making it difficult to provide
|
||||
standard documentation since these actions vary by device. For more
|
||||
information, see: https://esphome.io/components/api.html#user-defined-actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
ESPHome relies on sleepy devices and fast reconnect logic, so we
|
||||
can't raise `ConfigEntryNotReady`. Instead, we need to utilize the
|
||||
reconnect logic in `aioesphomeapi` to determine the right moment
|
||||
to trigger the connection.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since ESPHome is a framework for creating custom devices, the
|
||||
possibilities are virtually limitless. As a result, example
|
||||
automations would likely only be relevant to the specific user
|
||||
of the device and not generally useful to others.
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -7,9 +7,6 @@ from typing import cast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.assist_pipeline.repair_flows import (
|
||||
AssistInProgressDeprecatedRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
@@ -99,8 +96,6 @@ async def async_create_fix_flow(
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
if issue_id.startswith("assist_in_progress_deprecated"):
|
||||
return AssistInProgressDeprecatedRepairFlow(data)
|
||||
if issue_id.startswith("device_conflict"):
|
||||
return DeviceConflictRepair(data)
|
||||
# If ESPHome adds confirm-only repairs in the future, this should be changed
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_setup_entry(
|
||||
[
|
||||
EsphomeAssistPipelineSelect(hass, entry_data),
|
||||
EsphomeVadSensitivitySelect(hass, entry_data),
|
||||
EsphomeAssistSatelliteWakeWordSelect(hass, entry_data),
|
||||
EsphomeAssistSatelliteWakeWordSelect(entry_data),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -107,11 +107,10 @@ class EsphomeAssistSatelliteWakeWordSelect(
|
||||
translation_key="wake_word",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
_attr_should_poll = False
|
||||
_attr_current_option: str | None = None
|
||||
_attr_options: list[str] = []
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
|
||||
def __init__(self, entry_data: RuntimeEntryData) -> None:
|
||||
"""Initialize a wake word selector."""
|
||||
EsphomeAssistEntity.__init__(self, entry_data)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
|
||||
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
|
||||
"reconfigure_already_configured": "A device `{name}` with MAC address `{mac}` is already configured as `{title}`. Reconfiguration was aborted because the new configuration appears to refer to a different device.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
||||
@@ -19,10 +20,11 @@
|
||||
"reconfigure_unique_id_changed": "**Reconfiguration of `{name}` was aborted** because the address `{host}` points to a different device: `{unexpected_device_name}` (MAC: `{unexpected_mac}`) instead of the expected one (MAC: `{expected_mac}`)."
|
||||
},
|
||||
"error": {
|
||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
||||
"connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
|
||||
"resolve_error": "Unable to resolve the address of the ESPHome device. If this issue continues, consider setting a static IP address.",
|
||||
"connection_error": "Unable to connect to the ESPHome device. Make sure the device’s YAML configuration includes an `api` section.",
|
||||
"requires_encryption_key": "The ESPHome device requires an encryption key. Enter the key defined in the device’s YAML configuration under `api -> encryption -> key`.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration"
|
||||
"invalid_psk": "The encryption key is invalid. Make sure it matches the value in the device’s YAML configuration under `api -> encryption -> key`."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
@@ -101,11 +103,6 @@
|
||||
"name": "[%key:component::assist_satellite::entity_component::_::name%]"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"assist_in_progress": {
|
||||
"name": "[%key:component::assist_pipeline::entity::binary_sensor::assist_in_progress::name%]"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"pipeline": {
|
||||
"name": "[%key:component::assist_pipeline::entity::select::pipeline::name%]",
|
||||
|
||||
@@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the switch is on."""
|
||||
return self._state.state
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def _async_setup_update_entity() -> None:
|
||||
"""Set up the update entity."""
|
||||
nonlocal unsubs
|
||||
assert dashboard is not None
|
||||
# Keep listening until device is available
|
||||
if not entry_data.available or not dashboard.last_update_success:
|
||||
@@ -95,10 +94,12 @@ async def async_setup_entry(
|
||||
_async_setup_update_entity()
|
||||
return
|
||||
|
||||
unsubs = [
|
||||
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
|
||||
dashboard.async_add_listener(_async_setup_update_entity),
|
||||
]
|
||||
unsubs.extend(
|
||||
[
|
||||
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
|
||||
dashboard.async_add_listener(_async_setup_update_entity),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ESPHomeDashboardUpdateEntity(
|
||||
@@ -109,7 +110,6 @@ class ESPHomeDashboardUpdateEntity(
|
||||
_attr_has_entity_name = True
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_title = "ESPHome"
|
||||
_attr_name = "Firmware"
|
||||
_attr_release_url = "https://esphome.io/changelog/"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
@@ -242,7 +242,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def installed_version(self) -> str | None:
|
||||
def installed_version(self) -> str:
|
||||
"""Return the installed version."""
|
||||
return self._state.current_version
|
||||
|
||||
@@ -260,19 +260,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def release_summary(self) -> str | None:
|
||||
def release_summary(self) -> str:
|
||||
"""Return the release summary."""
|
||||
return self._state.release_summary
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def release_url(self) -> str | None:
|
||||
def release_url(self) -> str:
|
||||
"""Return the release URL."""
|
||||
return self._state.release_url
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def title(self) -> str | None:
|
||||
def title(self) -> str:
|
||||
"""Return the title of the update."""
|
||||
return self._state.title
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def current_valve_position(self) -> int | None:
|
||||
def current_valve_position(self) -> int:
|
||||
"""Return current position of valve. 0 is closed, 100 is open."""
|
||||
return round(self._state.position * 100.0)
|
||||
|
||||
|
||||
@@ -72,7 +72,11 @@ async def get_hosts_list_if_supported(
|
||||
supports_hosts: bool = True
|
||||
fbx_devices: list[dict[str, Any]] = []
|
||||
try:
|
||||
fbx_devices = await fbx_api.lan.get_hosts_list() or []
|
||||
fbx_interfaces = await fbx_api.lan.get_interfaces() or []
|
||||
for interface in fbx_interfaces:
|
||||
fbx_devices.extend(
|
||||
await fbx_api.lan.get_hosts_list(interface["name"]) or []
|
||||
)
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, ValuesView
|
||||
from collections.abc import Callable, Mapping, ValuesView
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
from fritzconnection import FritzConnection
|
||||
@@ -187,7 +186,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
)
|
||||
|
||||
self._devices: dict[str, FritzDevice] = {}
|
||||
self._options: MappingProxyType[str, Any] | None = None
|
||||
self._options: Mapping[str, Any] | None = None
|
||||
self._unique_id: str | None = None
|
||||
self.connection: FritzConnection = None
|
||||
self.fritz_guest_wifi: FritzGuestWLAN = None
|
||||
@@ -213,9 +212,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
str, Callable[[FritzStatus, StateType], Any]
|
||||
] = {}
|
||||
|
||||
async def async_setup(
|
||||
self, options: MappingProxyType[str, Any] | None = None
|
||||
) -> None:
|
||||
async def async_setup(self, options: Mapping[str, Any] | None = None) -> None:
|
||||
"""Wrap up FritzboxTools class setup."""
|
||||
self._options = options
|
||||
await self.hass.async_add_executor_job(self.setup)
|
||||
|
||||
@@ -22,19 +22,14 @@ from .entity import FritzBoxDeviceEntity
|
||||
from .model import FritzEntityDescriptionMixinBase
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FritzEntityDescriptionMixinBinarySensor(FritzEntityDescriptionMixinBase):
|
||||
"""BinarySensor description mixin for Fritz!Smarthome entities."""
|
||||
|
||||
is_on: Callable[[FritzhomeDevice], bool | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription, FritzEntityDescriptionMixinBinarySensor
|
||||
BinarySensorEntityDescription, FritzEntityDescriptionMixinBase
|
||||
):
|
||||
"""Description for Fritz!Smarthome binary sensor entities."""
|
||||
|
||||
is_on: Callable[[FritzhomeDevice], bool | None]
|
||||
|
||||
|
||||
BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
|
||||
FritzBinarySensorEntityDescription(
|
||||
|
||||
@@ -53,8 +53,11 @@ MAX_TEMPERATURE = 28
|
||||
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
|
||||
ON_API_TEMPERATURE = 127.0
|
||||
OFF_API_TEMPERATURE = 126.5
|
||||
ON_REPORT_SET_TEMPERATURE = 30.0
|
||||
OFF_REPORT_SET_TEMPERATURE = 0.0
|
||||
PRESET_API_HKR_STATE_MAPPING = {
|
||||
PRESET_COMFORT: "comfort",
|
||||
PRESET_BOOST: "on",
|
||||
PRESET_ECO: "eco",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -128,29 +131,28 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return self.data.actual_temperature # type: ignore [no-any-return]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
||||
return ON_REPORT_SET_TEMPERATURE
|
||||
if self.data.target_temperature == OFF_API_TEMPERATURE:
|
||||
return OFF_REPORT_SET_TEMPERATURE
|
||||
if self.data.target_temperature in [ON_API_TEMPERATURE, OFF_API_TEMPERATURE]:
|
||||
return None
|
||||
return self.data.target_temperature # type: ignore [no-any-return]
|
||||
|
||||
async def async_set_hkr_state(self, hkr_state: str) -> None:
|
||||
"""Set the state of the climate."""
|
||||
await self.hass.async_add_executor_job(self.data.set_hkr_state, hkr_state, True)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||
await self.async_set_hkr_state("off")
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
if target_temp == OFF_API_TEMPERATURE:
|
||||
target_temp = OFF_REPORT_SET_TEMPERATURE
|
||||
elif target_temp == ON_API_TEMPERATURE:
|
||||
target_temp = ON_REPORT_SET_TEMPERATURE
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.set_target_temperature, target_temp, True
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
return
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
@@ -159,10 +161,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return HVACMode.HEAT
|
||||
if self.data.summer_active:
|
||||
return HVACMode.OFF
|
||||
if self.data.target_temperature in (
|
||||
OFF_REPORT_SET_TEMPERATURE,
|
||||
OFF_API_TEMPERATURE,
|
||||
):
|
||||
if self.data.target_temperature == OFF_API_TEMPERATURE:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.HEAT
|
||||
@@ -180,7 +179,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
)
|
||||
return
|
||||
if hvac_mode is HVACMode.OFF:
|
||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
await self.async_set_hkr_state("off")
|
||||
else:
|
||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||
target_temp = self.data.eco_temperature
|
||||
@@ -210,12 +209,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_preset_while_active_mode",
|
||||
)
|
||||
if preset_mode == PRESET_COMFORT:
|
||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
elif preset_mode == PRESET_BOOST:
|
||||
await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
|
||||
@@ -35,20 +35,14 @@ from .entity import FritzBoxDeviceEntity
|
||||
from .model import FritzEntityDescriptionMixinBase
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase):
|
||||
"""Sensor description mixin for Fritz!Smarthome entities."""
|
||||
|
||||
native_value: Callable[[FritzhomeDevice], StateType | datetime]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzSensorEntityDescription(
|
||||
SensorEntityDescription, FritzEntityDescriptionMixinSensor
|
||||
SensorEntityDescription, FritzEntityDescriptionMixinBase
|
||||
):
|
||||
"""Description for Fritz!Smarthome sensor entities."""
|
||||
|
||||
entity_category_fn: Callable[[FritzhomeDevice], EntityCategory | None] | None = None
|
||||
native_value: Callable[[FritzhomeDevice], StateType | datetime]
|
||||
|
||||
|
||||
def suitable_eco_temperature(device: FritzhomeDevice) -> bool:
|
||||
|
||||
@@ -317,11 +317,11 @@
|
||||
"state_message": {
|
||||
"name": "State message",
|
||||
"state": {
|
||||
"fault": "[%key:common::state::fault%]",
|
||||
"critical_fault": "Critical fault",
|
||||
"up_and_running": "Up and running",
|
||||
"keep_minimum_temperature": "Keep minimum temperature",
|
||||
"legionella_protection": "Legionella protection",
|
||||
"critical_fault": "Critical fault",
|
||||
"fault": "Fault",
|
||||
"boost_mode": "Boost mode"
|
||||
}
|
||||
},
|
||||
@@ -362,7 +362,7 @@
|
||||
"name": "Relative autonomy"
|
||||
},
|
||||
"relative_self_consumption": {
|
||||
"name": "Relative self consumption"
|
||||
"name": "Relative self-consumption"
|
||||
},
|
||||
"capacity_maximum": {
|
||||
"name": "Maximum capacity"
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.0.2"]
|
||||
"requirements": ["odp-amsterdam==6.1.1"]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Geofency Webhook",
|
||||
"description": "Are you sure you want to set up the Geofency Webhook?"
|
||||
"title": "Set up the Geofency webhook",
|
||||
"description": "Are you sure you want to set up the Geofency webhook?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
119
homeassistant/components/google/quality_scale.yaml
Normal file
119
homeassistant/components/google/quality_scale.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
rules:
|
||||
# Bronze
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: Some fields missing data_description in the option flow.
|
||||
brands: done
|
||||
dependency-transparency:
|
||||
status: todo
|
||||
comment: |
|
||||
This depends on the legacy (deprecated) oauth libraries for device
|
||||
auth (no longer recommended auth). Google publishes to pypi using
|
||||
an internal build system. We need to either revisit approach or
|
||||
revisit our stance on this.
|
||||
common-modules: done
|
||||
has-entity-name: done
|
||||
action-setup:
|
||||
status: todo
|
||||
comment: |
|
||||
Actions are current setup in `async_setup_entry` and need to be moved
|
||||
to `async_setup`.
|
||||
appropriate-polling: done
|
||||
test-before-configure: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Integration does not subscribe to events.
|
||||
unique-config-entry: done
|
||||
entity-unique-id: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
test-before-setup:
|
||||
status: todo
|
||||
comment: |
|
||||
The integration does not test the connection in `async_setup_entry` but
|
||||
instead does this in the calendar platform only, which can be improved.
|
||||
docs-high-level-description: done
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
The config flow has 100% test coverage, however there are opportunities
|
||||
to increase functionality such as checking for the specific contents
|
||||
of a unique id assigned to a config entry.
|
||||
docs-actions: done
|
||||
runtime-data:
|
||||
status: todo
|
||||
comment: |
|
||||
The integration stores config entry data in `hass.data` and should be
|
||||
updated to use `runtime_data`.
|
||||
|
||||
# Silver
|
||||
log-when-unavailable: done
|
||||
config-entry-unloading: done
|
||||
reauthentication-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
The integration supports reauthentication, however the config flow test
|
||||
coverage can be improved on reauth corner cases.
|
||||
action-exceptions: done
|
||||
docs-installation-parameters: todo
|
||||
integration-owner: done
|
||||
parallel-updates: todo
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: One module needs an additional line of coverage to be above the bar
|
||||
docs-configuration-parameters: todo
|
||||
entity-unavailable: done
|
||||
|
||||
# Gold
|
||||
docs-examples: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Google calendar does not support discovery
|
||||
entity-device-class: todo
|
||||
entity-translations: todo
|
||||
docs-data-update: todo
|
||||
entity-disabled-by-default: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Google calendar does not support discovery
|
||||
exception-translations: todo
|
||||
devices: todo
|
||||
docs-supported-devices: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: Google calendar does not have any icons
|
||||
docs-known-limitations: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Google calendar does not have devices
|
||||
docs-supported-functions: done
|
||||
repair-issues:
|
||||
status: todo
|
||||
comment: There are some warnings/deprecations that should be repair issues
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: There is nothing to configure in the configuration flow
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: The entities in google calendar do not support categories
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Google calendar does not have devices
|
||||
docs-troubleshooting: todo
|
||||
diagnostics: todo
|
||||
docs-use-cases: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: done
|
||||
comment: |
|
||||
The main client `gcal_sync` library is async. The primary authentication
|
||||
used in config flow is handled by built in async OAuth code. The
|
||||
integration still supports legacy OAuth credentials setup in the
|
||||
configuration flow, which is no longer recommended or described in the
|
||||
documentation for new users. This legacy config flow uses oauth2client
|
||||
which is not natively async.
|
||||
strict-typing:
|
||||
status: todo
|
||||
comment: Dependency oauth2client does not confirm to PEP 561
|
||||
inject-websession: done
|
||||
@@ -208,7 +208,7 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
|
||||
async def google_generative_ai_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
||||
options: Mapping[str, Any],
|
||||
genai_client: genai.Client,
|
||||
) -> dict:
|
||||
"""Return a schema for Google Generative AI completion options."""
|
||||
|
||||
@@ -16,7 +16,7 @@ RECOMMENDED_TOP_P = 0.95
|
||||
CONF_TOP_K = "top_k"
|
||||
RECOMMENDED_TOP_K = 64
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
RECOMMENDED_MAX_TOKENS = 150
|
||||
RECOMMENDED_MAX_TOKENS = 1500
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD = "harassment_block_threshold"
|
||||
CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold"
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user