mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 06:48:13 +00:00
Compare commits
806 Commits
2025.12.0b
...
schedule/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51ff1ca2d8 | ||
|
|
c418d9750b | ||
|
|
e96d614076 | ||
|
|
f0a5e0a023 | ||
|
|
6ac6b86060 | ||
|
|
3909171b1a | ||
|
|
769029505f | ||
|
|
080ec3524b | ||
|
|
48d671ad5f | ||
|
|
7115db5d22 | ||
|
|
d0c8792e4b | ||
|
|
84d7c37502 | ||
|
|
8a10638470 | ||
|
|
10dd53ffc2 | ||
|
|
36aefce9e1 | ||
|
|
fe34da19e2 | ||
|
|
fe94dea1db | ||
|
|
3f57b46756 | ||
|
|
7e141533bb | ||
|
|
391ccbafae | ||
|
|
6af674e64e | ||
|
|
7b1653c77b | ||
|
|
c87dafa2e6 | ||
|
|
8375acf315 | ||
|
|
4df5a41b57 | ||
|
|
5796b4c0d9 | ||
|
|
5f4f07803b | ||
|
|
a0a444e3c8 | ||
|
|
30cfe987ed | ||
|
|
412ee0da05 | ||
|
|
d6b675138d | ||
|
|
bde3cef17d | ||
|
|
412ee30584 | ||
|
|
7eecdc87fd | ||
|
|
9ba252d8e3 | ||
|
|
1709a9d255 | ||
|
|
bcf46f09a2 | ||
|
|
d4097a8686 | ||
|
|
2a92292e76 | ||
|
|
fe987a63d6 | ||
|
|
91f3b991ba | ||
|
|
46c6313068 | ||
|
|
86e4a81934 | ||
|
|
234d6ae161 | ||
|
|
2ab203618e | ||
|
|
faae23ee1b | ||
|
|
f6acd4f230 | ||
|
|
71d36a6496 | ||
|
|
9fc014c6f4 | ||
|
|
537f93872c | ||
|
|
06a55175a8 | ||
|
|
5f37016baa | ||
|
|
1af884293f | ||
|
|
ba73ab38e8 | ||
|
|
2d33a720f7 | ||
|
|
dbfdaf6a2e | ||
|
|
278cb4d3ae | ||
|
|
1c6f8b7e54 | ||
|
|
731f5078a6 | ||
|
|
9863d3484d | ||
|
|
f85a684e31 | ||
|
|
e292a67692 | ||
|
|
c82d159c14 | ||
|
|
d890387d3d | ||
|
|
d996d7b113 | ||
|
|
d28a4598d5 | ||
|
|
229f7c4f37 | ||
|
|
9f2138aa18 | ||
|
|
7506ff826c | ||
|
|
317a3ed044 | ||
|
|
d7801881e9 | ||
|
|
a4bbdafd55 | ||
|
|
97673f22cb | ||
|
|
d63cdafad2 | ||
|
|
50f47a7397 | ||
|
|
123d573274 | ||
|
|
64ccde6709 | ||
|
|
c69ef7e1f6 | ||
|
|
d51cca3325 | ||
|
|
2679ac3f5e | ||
|
|
47f476af32 | ||
|
|
ca3d03131e | ||
|
|
a3f3586b02 | ||
|
|
0ced960d1d | ||
|
|
78f1b434b3 | ||
|
|
563fa8f958 | ||
|
|
8de6f04829 | ||
|
|
f74128de49 | ||
|
|
25e1cc42eb | ||
|
|
245d57be1a | ||
|
|
c09c016299 | ||
|
|
d9283ad4cd | ||
|
|
11f319c79c | ||
|
|
cc5d98fe8b | ||
|
|
3e1f9de0de | ||
|
|
34212c6e65 | ||
|
|
10f02d040f | ||
|
|
1114ce8509 | ||
|
|
8687a7b306 | ||
|
|
d325f677df | ||
|
|
f786ec18a9 | ||
|
|
ef0add1d6c | ||
|
|
46f56c60f2 | ||
|
|
060b258921 | ||
|
|
8eb3e63d9d | ||
|
|
6c96acda82 | ||
|
|
c82b179e03 | ||
|
|
1e1265c99c | ||
|
|
22e975f911 | ||
|
|
b1e3f8d4f4 | ||
|
|
95fe573620 | ||
|
|
d29d82cf20 | ||
|
|
23459d69c9 | ||
|
|
5209f4d296 | ||
|
|
39c5983571 | ||
|
|
b5015faffe | ||
|
|
16d3707d13 | ||
|
|
9bb6d740e0 | ||
|
|
f640795de1 | ||
|
|
c1b512d50a | ||
|
|
0ba43b22a9 | ||
|
|
b11b790958 | ||
|
|
81fb769233 | ||
|
|
c5ac806832 | ||
|
|
256baf5097 | ||
|
|
cccefddb72 | ||
|
|
85294b1d96 | ||
|
|
ffcde8dd74 | ||
|
|
13b50b355e | ||
|
|
7a2592173b | ||
|
|
d3b36d0081 | ||
|
|
5bea0d57ec | ||
|
|
903c73b5dd | ||
|
|
603c664c9b | ||
|
|
43d0d582ef | ||
|
|
c12d2ec0bd | ||
|
|
bfedcef9b9 | ||
|
|
d41d93c44e | ||
|
|
11cc8beac1 | ||
|
|
0f827403c5 | ||
|
|
4827a603e5 | ||
|
|
e3f3861d4e | ||
|
|
60df4433ca | ||
|
|
46b6557348 | ||
|
|
639e736c66 | ||
|
|
8a198401a7 | ||
|
|
c917dfeed9 | ||
|
|
25155de30c | ||
|
|
6a927c37be | ||
|
|
9b41bb09a7 | ||
|
|
e58fc6976d | ||
|
|
9def627a57 | ||
|
|
9b56759c1e | ||
|
|
c3f743cafd | ||
|
|
160c495ddc | ||
|
|
9a1cd8545d | ||
|
|
fa81e6cd04 | ||
|
|
746f4ef1e2 | ||
|
|
0149de6ba6 | ||
|
|
1df2f18e0a | ||
|
|
5800824893 | ||
|
|
aac07b6b4b | ||
|
|
2448ce1970 | ||
|
|
7b40e3b8a7 | ||
|
|
29d06cfcc9 | ||
|
|
365d168ddd | ||
|
|
234191336e | ||
|
|
ba57b72658 | ||
|
|
bb08b315b8 | ||
|
|
50621df244 | ||
|
|
2db7b5c99f | ||
|
|
78af3acf35 | ||
|
|
b72f04d44e | ||
|
|
35f287e330 | ||
|
|
0a55f83b46 | ||
|
|
5030d0ba90 | ||
|
|
f582f06ee4 | ||
|
|
662bada5d8 | ||
|
|
3ca338dd25 | ||
|
|
9337a0e71b | ||
|
|
ccbb00197d | ||
|
|
0f59c17e61 | ||
|
|
6253ade3e2 | ||
|
|
e5890378a1 | ||
|
|
b8ab0bcadf | ||
|
|
19cb827577 | ||
|
|
03676d7e5a | ||
|
|
13f3b49b96 | ||
|
|
90c8c56a06 | ||
|
|
afb9e18a7d | ||
|
|
2c2934065f | ||
|
|
0bead67df9 | ||
|
|
2895849203 | ||
|
|
b2400708ac | ||
|
|
0bed9c20b3 | ||
|
|
d3fb7a7b87 | ||
|
|
60dcca4143 | ||
|
|
01f498f239 | ||
|
|
15055b8e8e | ||
|
|
6826619e12 | ||
|
|
b50a8e04a8 | ||
|
|
c6c67c5357 | ||
|
|
c82803d1e2 | ||
|
|
732b30f181 | ||
|
|
0e2e57a657 | ||
|
|
f00b0080a9 | ||
|
|
ad970c1234 | ||
|
|
02ec56bffa | ||
|
|
8388c290bf | ||
|
|
576ee99faf | ||
|
|
8a3534c345 | ||
|
|
e1e91c5568 | ||
|
|
1e09bddb1d | ||
|
|
90e4340595 | ||
|
|
120b17349c | ||
|
|
8a26961304 | ||
|
|
407b675080 | ||
|
|
274844271b | ||
|
|
f11e4e7bda | ||
|
|
96f8c39c6f | ||
|
|
77b79fef8d | ||
|
|
a0d2f285f3 | ||
|
|
3aef05d1ec | ||
|
|
510e391ee4 | ||
|
|
54adfdd694 | ||
|
|
d45f920b4a | ||
|
|
3080ef9a4a | ||
|
|
51cebb52f3 | ||
|
|
7b0d4c47b7 | ||
|
|
a660ab3f97 | ||
|
|
dd8fc16788 | ||
|
|
2b0fab0468 | ||
|
|
3bb88ed433 | ||
|
|
984385cd98 | ||
|
|
09de108676 | ||
|
|
ebc7581718 | ||
|
|
e55162812d | ||
|
|
aa6ccaa024 | ||
|
|
e1b009a6de | ||
|
|
91ddc525b0 | ||
|
|
d7d7954ac2 | ||
|
|
e87c260df7 | ||
|
|
5185c6cd68 | ||
|
|
7599c918e2 | ||
|
|
fa7e22ec91 | ||
|
|
606519e51b | ||
|
|
8e39e010f7 | ||
|
|
dc01cf49a0 | ||
|
|
1f3ad382f1 | ||
|
|
2595c7dcb2 | ||
|
|
d445b320de | ||
|
|
7b6df1a8a0 | ||
|
|
2a151dcd19 | ||
|
|
adbab150af | ||
|
|
d20edf7928 | ||
|
|
7d6d37fe76 | ||
|
|
228e0453a7 | ||
|
|
1da31c0530 | ||
|
|
41ad15e577 | ||
|
|
421af881fe | ||
|
|
715a484f7e | ||
|
|
0a789f51b8 | ||
|
|
fa25d45123 | ||
|
|
6d255b2521 | ||
|
|
5ffb39f064 | ||
|
|
d642109436 | ||
|
|
10f6d8d14f | ||
|
|
a94678cb06 | ||
|
|
0d8d466003 | ||
|
|
8ddf3e1734 | ||
|
|
d88047a750 | ||
|
|
61c7ac81d6 | ||
|
|
bbe07bddb0 | ||
|
|
a3afc2beb1 | ||
|
|
374cd93d3d | ||
|
|
6e99411084 | ||
|
|
41d5415c86 | ||
|
|
052d56f358 | ||
|
|
0a676b5812 | ||
|
|
1f4cf67daa | ||
|
|
bb4ec229ce | ||
|
|
ff62b460d5 | ||
|
|
9b48e92940 | ||
|
|
c03b9d1f87 | ||
|
|
3f30df203c | ||
|
|
7fe0d96c88 | ||
|
|
cdc2192bba | ||
|
|
74b1c1f6fd | ||
|
|
69c7a7b0ab | ||
|
|
ef302215cc | ||
|
|
6378f5f02a | ||
|
|
79245195cd | ||
|
|
d0e33a6e04 | ||
|
|
f55fc788db | ||
|
|
6152e0fa27 | ||
|
|
f1a89741c0 | ||
|
|
7629c9f280 | ||
|
|
6b8650c6d9 | ||
|
|
48f186368a | ||
|
|
d65baac8d4 | ||
|
|
d57801407b | ||
|
|
4495a76557 | ||
|
|
99dfb93ac0 | ||
|
|
7c7c0aad25 | ||
|
|
5992898340 | ||
|
|
4f2ff9a4f4 | ||
|
|
a8a135c2ca | ||
|
|
43e241ee39 | ||
|
|
6af7052b9d | ||
|
|
c0aa35ff6d | ||
|
|
2c7763e350 | ||
|
|
95e344ea44 | ||
|
|
7ed8613411 | ||
|
|
4ac0567ccc | ||
|
|
bc031e7a81 | ||
|
|
ad1ba629c5 | ||
|
|
0c2cb460cb | ||
|
|
5388740c83 | ||
|
|
2a54d4c3a9 | ||
|
|
2008972215 | ||
|
|
39004bd0a2 | ||
|
|
bb847ce3ff | ||
|
|
05920a9c73 | ||
|
|
61499a5ad4 | ||
|
|
0076aafa6e | ||
|
|
c50f4d6d2d | ||
|
|
68036099a2 | ||
|
|
180053fe98 | ||
|
|
280c25cb85 | ||
|
|
4064b6d28c | ||
|
|
ff25809a3e | ||
|
|
245f47c7fb | ||
|
|
86135a19d1 | ||
|
|
2e038250a9 | ||
|
|
88c7c6fc8a | ||
|
|
d691862d0d | ||
|
|
cceaff7bc6 | ||
|
|
079c6daa63 | ||
|
|
b120ae827f | ||
|
|
c1227aaf1f | ||
|
|
c0365dfe99 | ||
|
|
02aa3fc906 | ||
|
|
42e55491cc | ||
|
|
33e09c4967 | ||
|
|
6f5507670f | ||
|
|
765be3f047 | ||
|
|
12bc9e9f68 | ||
|
|
2617c4a453 | ||
|
|
0e6d9ecbdc | ||
|
|
5cdbbe999d | ||
|
|
5ca61386f8 | ||
|
|
6d6ee866a6 | ||
|
|
eeb2b2febc | ||
|
|
a6c7bd76eb | ||
|
|
470f5a2396 | ||
|
|
d934fd974d | ||
|
|
edc81b706d | ||
|
|
03aaebe718 | ||
|
|
98d61aa5b2 | ||
|
|
fe5d411856 | ||
|
|
efa5a773eb | ||
|
|
32399de5f1 | ||
|
|
a1ad28c066 | ||
|
|
6faccf4327 | ||
|
|
2ac15ab67d | ||
|
|
d599bb9553 | ||
|
|
92ee37017d | ||
|
|
adf698d570 | ||
|
|
6ce9a13816 | ||
|
|
9cb9efeb88 | ||
|
|
ca31134caa | ||
|
|
769578dc51 | ||
|
|
9dcabfe804 | ||
|
|
dc6c23a58c | ||
|
|
6ec7efc2b8 | ||
|
|
97e5b7954e | ||
|
|
25505752b7 | ||
|
|
95a347dcf8 | ||
|
|
8c0f3014f7 | ||
|
|
bb3cd3ebd3 | ||
|
|
319d6711c4 | ||
|
|
ea3f76c315 | ||
|
|
b892cc1cad | ||
|
|
3046c7afd8 | ||
|
|
73dc81034e | ||
|
|
f306cde3b6 | ||
|
|
38c5e483a8 | ||
|
|
ce14544ec1 | ||
|
|
87b9c3193e | ||
|
|
061c38d2a7 | ||
|
|
e1720be5a4 | ||
|
|
2d13a92496 | ||
|
|
b06bffa815 | ||
|
|
b8f4b9515b | ||
|
|
3c10e9f1c0 | ||
|
|
2dec3befcd | ||
|
|
7d065bf314 | ||
|
|
3315680d0b | ||
|
|
ce48c89a26 | ||
|
|
f67a926f56 | ||
|
|
e0a9d305b2 | ||
|
|
4ff141d35e | ||
|
|
f12a43b2b7 | ||
|
|
35e6f504a3 | ||
|
|
1f68809cf9 | ||
|
|
66bddebca1 | ||
|
|
2280d779a8 | ||
|
|
ebc608845c | ||
|
|
5d13a41926 | ||
|
|
630b40fbba | ||
|
|
7fd440c4a0 | ||
|
|
2a116a2a11 | ||
|
|
f189e3b5ca | ||
|
|
4cd460351d | ||
|
|
afea571c2c | ||
|
|
e4aadd675e | ||
|
|
a47255c233 | ||
|
|
c1e7492743 | ||
|
|
63e8cf582f | ||
|
|
73f23168a2 | ||
|
|
20d8176515 | ||
|
|
c9351a022e | ||
|
|
4e8a31a4e2 | ||
|
|
2beb551db3 | ||
|
|
90cea0325f | ||
|
|
f5dd9d83ac | ||
|
|
e0484ba1ff | ||
|
|
62f758f695 | ||
|
|
20d2115122 | ||
|
|
2bed7afe0e | ||
|
|
2eeac5f9c9 | ||
|
|
a35af9097b | ||
|
|
710b7c2b41 | ||
|
|
c058810461 | ||
|
|
0ccfd77fef | ||
|
|
4805b33a27 | ||
|
|
c333036959 | ||
|
|
002eed24f1 | ||
|
|
9a9f8271b3 | ||
|
|
855d7c6e16 | ||
|
|
837de55ce6 | ||
|
|
81ed259c59 | ||
|
|
5f00452c96 | ||
|
|
06a44de3fb | ||
|
|
11b4d75cfb | ||
|
|
845c9ee05f | ||
|
|
dedf6b1223 | ||
|
|
c1b631d049 | ||
|
|
6cc645bc6c | ||
|
|
f10866395d | ||
|
|
df68448b27 | ||
|
|
bf7b96622c | ||
|
|
53c644ac5b | ||
|
|
5e9107e52b | ||
|
|
ca9ea267c7 | ||
|
|
f1bfe2f11e | ||
|
|
34cc6036b9 | ||
|
|
2facfbadaa | ||
|
|
1b1dface35 | ||
|
|
3c0cfd5e0c | ||
|
|
69f66ffef4 | ||
|
|
d2c3543b6c | ||
|
|
ca4a2d441e | ||
|
|
f42fe9cee3 | ||
|
|
b67873f40c | ||
|
|
ecc08fce0f | ||
|
|
375f536b15 | ||
|
|
5cff813eac | ||
|
|
c2ce322af1 | ||
|
|
079f306a65 | ||
|
|
9129665c64 | ||
|
|
7bf60f9d15 | ||
|
|
7dddd89ac2 | ||
|
|
a2322ef3c7 | ||
|
|
5f6ef2109a | ||
|
|
44f0a8899a | ||
|
|
78fa29b41f | ||
|
|
06d4f085c0 | ||
|
|
f4e11da1a6 | ||
|
|
e0238b5ab2 | ||
|
|
352f3813e2 | ||
|
|
b1399a5541 | ||
|
|
316cddec86 | ||
|
|
2f71aec26f | ||
|
|
aa72b76ee7 | ||
|
|
e009898107 | ||
|
|
ceb13e70b9 | ||
|
|
498a80ac7f | ||
|
|
a9deb2a08a | ||
|
|
0d26d22986 | ||
|
|
062366966b | ||
|
|
1a60c46d67 | ||
|
|
62fba5ca20 | ||
|
|
b54cde795c | ||
|
|
0f456373bf | ||
|
|
a5042027b8 | ||
|
|
1b8a50e80a | ||
|
|
59761385f0 | ||
|
|
6536d348e5 | ||
|
|
c157c83d54 | ||
|
|
77425cc40f | ||
|
|
b15b5ba95c | ||
|
|
cd6e72798e | ||
|
|
739157e59f | ||
|
|
267aa1af42 | ||
|
|
7328b61a69 | ||
|
|
c4b67329c3 | ||
|
|
c1f8c89bd0 | ||
|
|
b1bf6f5678 | ||
|
|
d347136188 | ||
|
|
a4319f3bf8 | ||
|
|
db27aee62a | ||
|
|
a7446b3da9 | ||
|
|
7fc5464621 | ||
|
|
a00b50c195 | ||
|
|
203f2fb364 | ||
|
|
b956c17ce4 | ||
|
|
5163dc0567 | ||
|
|
31a0478717 | ||
|
|
24da3f0db8 | ||
|
|
786922fc5d | ||
|
|
c2f8b6986b | ||
|
|
0a0832671f | ||
|
|
7b353d7ad4 | ||
|
|
99de73a729 | ||
|
|
1995fbd252 | ||
|
|
315ea9dc76 | ||
|
|
639a96f8cb | ||
|
|
b6786c5a42 | ||
|
|
6f6e9b8057 | ||
|
|
e0c687e415 | ||
|
|
982362110c | ||
|
|
90dc3a8fdf | ||
|
|
5112742b71 | ||
|
|
8899bc01bd | ||
|
|
738fb59efa | ||
|
|
04e512a48e | ||
|
|
c63aca2d9b | ||
|
|
c95203e095 | ||
|
|
259235ceeb | ||
|
|
c7f1729300 | ||
|
|
065329e668 | ||
|
|
a93ed69fe4 | ||
|
|
189497622d | ||
|
|
ed8f9105ff | ||
|
|
185de98f5e | ||
|
|
e857abb43f | ||
|
|
5b1829f3a1 | ||
|
|
520156a33a | ||
|
|
e3b5342b76 | ||
|
|
951b19e80c | ||
|
|
e2351ecec2 | ||
|
|
d75e5498c6 | ||
|
|
2dd58dbe39 | ||
|
|
4ef17799db | ||
|
|
9373378350 | ||
|
|
18833a194b | ||
|
|
2631c77bee | ||
|
|
c67247bf32 | ||
|
|
18b5ffd365 | ||
|
|
a466fc4a01 | ||
|
|
8a968b5d0e | ||
|
|
3baee5c4ac | ||
|
|
f624a43770 | ||
|
|
242935774b | ||
|
|
051ad5878f | ||
|
|
b2156c1d4c | ||
|
|
7d4394f7ed | ||
|
|
4df172374c | ||
|
|
c97755472e | ||
|
|
ebc9060b01 | ||
|
|
bbcc2a94b3 | ||
|
|
692188fa85 | ||
|
|
2c993ea5a2 | ||
|
|
c4e3a4d65e | ||
|
|
84d2686517 | ||
|
|
ae8980ce5b | ||
|
|
b2d4c9ecb4 | ||
|
|
f5b046ee7d | ||
|
|
55c5fb7374 | ||
|
|
5d78cd328a | ||
|
|
bc36578ada | ||
|
|
e63242e465 | ||
|
|
e84c09745d | ||
|
|
f07991d0ba | ||
|
|
872fef1f6f | ||
|
|
c866dc973c | ||
|
|
e2acf30637 | ||
|
|
29631a2c5a | ||
|
|
1d31e6d0ea | ||
|
|
c765776726 | ||
|
|
723365d8e6 | ||
|
|
3d8e136049 | ||
|
|
2fe9fc7ee3 | ||
|
|
e11e31a1a0 | ||
|
|
989407047d | ||
|
|
6d3087c5a4 | ||
|
|
9bd3c35231 | ||
|
|
b7e97971cf | ||
|
|
4d232c63f8 | ||
|
|
6fc000ee2a | ||
|
|
623d3ecde5 | ||
|
|
0fbb3215b4 | ||
|
|
c82ce1ff89 | ||
|
|
8c891a20e5 | ||
|
|
97c50b2d86 | ||
|
|
ef4062a565 | ||
|
|
e31cce5d9b | ||
|
|
21f6b9a53a | ||
|
|
047e549112 | ||
|
|
4c4aecd9a7 | ||
|
|
733496ff3f | ||
|
|
f682e93243 | ||
|
|
c8fa5b0290 | ||
|
|
8ff2a22664 | ||
|
|
c174ab2d96 | ||
|
|
10f0ff7bd7 | ||
|
|
4a4eb33bf7 | ||
|
|
8199c4e5de | ||
|
|
0bfa8318a7 | ||
|
|
ed66a4920c | ||
|
|
f51007c448 | ||
|
|
bd44402b04 | ||
|
|
99fa92d966 | ||
|
|
1cb8f19020 | ||
|
|
81cdbdd4df | ||
|
|
8109d9a39c | ||
|
|
e1abd451b8 | ||
|
|
2c72cd94f2 | ||
|
|
3bccb4b89c | ||
|
|
6d4fb30630 | ||
|
|
c04411f1bc | ||
|
|
753ea023de | ||
|
|
1ca1cf59eb | ||
|
|
5b01bb1a29 | ||
|
|
15c89d24eb | ||
|
|
b26b2347e6 | ||
|
|
7d54103c09 | ||
|
|
c705a1dc4b | ||
|
|
998bd23446 | ||
|
|
3a1a58d6ad | ||
|
|
f9219dd841 | ||
|
|
402ed7e0f3 | ||
|
|
7a1a5df89e | ||
|
|
df558fc1e7 | ||
|
|
ec66407ef1 | ||
|
|
6b99234a43 | ||
|
|
393be71009 | ||
|
|
12bc1687ec | ||
|
|
c59b322c0a | ||
|
|
e00266463d | ||
|
|
cbc8a33553 | ||
|
|
28582f75d4 | ||
|
|
39cccd212d | ||
|
|
329ea33337 | ||
|
|
521733c420 | ||
|
|
33e9f9a0ff | ||
|
|
5fda2bccbe | ||
|
|
ae75332656 | ||
|
|
b171785f96 | ||
|
|
ff3d6783c6 | ||
|
|
b1e579bea0 | ||
|
|
87241ea051 | ||
|
|
a871ec0bdf | ||
|
|
b8829b645a | ||
|
|
5b056a83d4 | ||
|
|
02a70123c1 | ||
|
|
5f6d2f537a | ||
|
|
5e04e9f04d | ||
|
|
56515ad7b5 | ||
|
|
a1fe2bf4fa | ||
|
|
b8fa8efd91 | ||
|
|
03557b5ef2 | ||
|
|
dafec8ce58 | ||
|
|
6ff3f74347 | ||
|
|
ddd8cf7fde | ||
|
|
1356eea52f | ||
|
|
6188e0e39b | ||
|
|
699fa1617d | ||
|
|
449f0fa5a5 | ||
|
|
2e008d2bb7 | ||
|
|
05dec2619d | ||
|
|
25a6778ba8 | ||
|
|
f564b8cb44 | ||
|
|
ce6bfdebfc | ||
|
|
f00a944ac1 | ||
|
|
3073a99ce6 | ||
|
|
8b04ce1328 | ||
|
|
39f76787ab | ||
|
|
e8acced335 | ||
|
|
758a30eebc | ||
|
|
faf94bea24 | ||
|
|
ff91c57228 | ||
|
|
3d2b506997 | ||
|
|
d3c1c28605 | ||
|
|
d4e1f7741d | ||
|
|
e713632eed | ||
|
|
060ad35ddc | ||
|
|
6c5dba40cd | ||
|
|
a04d595424 | ||
|
|
fe85eaf2a2 | ||
|
|
3551c4b01f | ||
|
|
e7edd51a65 | ||
|
|
0c4f2326ef | ||
|
|
81f4456d7c | ||
|
|
2b608bf15c | ||
|
|
972ed4b27f | ||
|
|
23c167da1b | ||
|
|
34d6938171 | ||
|
|
4bb8590076 | ||
|
|
5e0923b60d | ||
|
|
ad48f3c634 | ||
|
|
2bdd6854eb | ||
|
|
0bf906911c | ||
|
|
874d6f5613 | ||
|
|
43ba10eebd | ||
|
|
64bed19805 | ||
|
|
6357067f0f | ||
|
|
e328ba4045 | ||
|
|
332dbddce6 | ||
|
|
82d935a819 | ||
|
|
4b84998c0c | ||
|
|
e10c1ebcf6 | ||
|
|
0174bad182 | ||
|
|
d5be623684 | ||
|
|
d006b044c8 | ||
|
|
fdd9571623 | ||
|
|
4f4c5152b9 | ||
|
|
b031a082cd | ||
|
|
a1132195fd | ||
|
|
708b3dc8b2 | ||
|
|
8ae0216135 | ||
|
|
1472281cd5 | ||
|
|
ceaa71d198 | ||
|
|
7f0d0c555a | ||
|
|
3b94b2491a | ||
|
|
8c8708d5bc | ||
|
|
ca35102138 | ||
|
|
1a1b50ef1a | ||
|
|
5a4d51e57a | ||
|
|
9e1bc637e2 | ||
|
|
ab879c07ca | ||
|
|
488c97531e | ||
|
|
3b52c5df79 | ||
|
|
7f4b56104d | ||
|
|
ab8135ba1a | ||
|
|
a88599bc09 | ||
|
|
45034279c8 | ||
|
|
9f3dae6254 | ||
|
|
ef36d7b1e5 | ||
|
|
e5346ba017 | ||
|
|
68d41d2a48 | ||
|
|
00a882c20a | ||
|
|
44a6772947 | ||
|
|
f874ba1355 | ||
|
|
4fc125c49a | ||
|
|
8c59196e19 | ||
|
|
326f7f0559 | ||
|
|
11afda8c22 | ||
|
|
f1ee0e4ac9 | ||
|
|
5f522e5afa | ||
|
|
4f6624d0aa | ||
|
|
70990645a7 | ||
|
|
2f7d74ff62 | ||
|
|
885667832b | ||
|
|
4646929987 | ||
|
|
010aea952c | ||
|
|
563678dc47 | ||
|
|
a48f01f213 | ||
|
|
08b758b0d2 | ||
|
|
4306fbea52 | ||
|
|
6f4c479f8f | ||
|
|
1d9c06264e | ||
|
|
d045ecaf13 | ||
|
|
f7c41e694c | ||
|
|
9ee7ed5cdb | ||
|
|
83c4e2abc9 | ||
|
|
a7dbf551a3 | ||
|
|
0b2bb9f6bf | ||
|
|
0769163b67 | ||
|
|
2bb51e1146 | ||
|
|
d2248d282c | ||
|
|
8fe79a88ca | ||
|
|
7a328539b2 | ||
|
|
ec69efee4d | ||
|
|
dbcde549d4 | ||
|
|
988355e138 | ||
|
|
7711eac607 | ||
|
|
32fe53cceb | ||
|
|
3a65d3c0dc | ||
|
|
7fe26223ac | ||
|
|
7e8496afb2 | ||
|
|
2ec5190243 | ||
|
|
a706db8fdb | ||
|
|
a00923c48b | ||
|
|
7480d59f0f | ||
|
|
4c8d9ed401 | ||
|
|
eef10c59db | ||
|
|
a1a1f8dd77 | ||
|
|
c75a5c5151 | ||
|
|
cdaaa2bd8f | ||
|
|
bd84dac8fb | ||
|
|
42cbeca5b0 | ||
|
|
ad0a498d10 | ||
|
|
973405822b | ||
|
|
b883d2f519 |
@@ -13,6 +13,7 @@ core: &core
|
|||||||
|
|
||||||
# Our base platforms, that are used by other integrations
|
# Our base platforms, that are used by other integrations
|
||||||
base_platforms: &base_platforms
|
base_platforms: &base_platforms
|
||||||
|
- homeassistant/components/ai_task/**
|
||||||
- homeassistant/components/air_quality/**
|
- homeassistant/components/air_quality/**
|
||||||
- homeassistant/components/alarm_control_panel/**
|
- homeassistant/components/alarm_control_panel/**
|
||||||
- homeassistant/components/assist_satellite/**
|
- homeassistant/components/assist_satellite/**
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"charliermarsh.ruff",
|
"charliermarsh.ruff",
|
||||||
"ms-python.pylint",
|
"ms-python.pylint",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
"visualstudioexptteam.vscodeintellicode",
|
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"GitHub.vscode-pull-request-github",
|
"GitHub.vscode-pull-request-github",
|
||||||
|
|||||||
50
.github/workflows/builder.yml
vendored
50
.github/workflows/builder.yml
vendored
@@ -15,7 +15,7 @@ env:
|
|||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
# Base image version from https://github.com/home-assistant/docker
|
# Base image version from https://github.com/home-assistant/docker
|
||||||
BASE_IMAGE_VERSION: "2025.11.3"
|
BASE_IMAGE_VERSION: "2025.12.0"
|
||||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
architectures: ${{ env.ARCHITECTURES }}
|
architectures: ${{ env.ARCHITECTURES }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -169,7 +169,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -190,7 +190,8 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install Cosign
|
- &install_cosign
|
||||||
|
name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.5.3"
|
cosign-release: "v2.5.3"
|
||||||
@@ -272,7 +273,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -294,7 +295,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/builder doesn't support sha pinning
|
# home-assistant/builder doesn't support sha pinning
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2025.09.0
|
uses: home-assistant/builder@2025.11.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -310,7 +311,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -353,10 +354,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Install Cosign
|
- *install_cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
with:
|
|
||||||
cosign-release: "v2.2.3"
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
@@ -393,7 +391,7 @@ jobs:
|
|||||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||||
- name: Generate Docker metadata
|
- name: Generate Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.registry }}/home-assistant
|
images: ${{ matrix.registry }}/home-assistant
|
||||||
sep-tags: ","
|
sep-tags: ","
|
||||||
@@ -407,7 +405,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.7.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
|
||||||
|
|
||||||
- name: Copy architecture images to DockerHub
|
- name: Copy architecture images to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
@@ -418,9 +416,19 @@ jobs:
|
|||||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||||
for arch in $ARCHS; do
|
for arch in $ARCHS; do
|
||||||
echo "Copying ${arch} image to DockerHub..."
|
echo "Copying ${arch} image to DockerHub..."
|
||||||
docker buildx imagetools create \
|
for attempt in 1 2 3; do
|
||||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
if docker buildx imagetools create \
|
||||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
||||||
|
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||||
|
sleep 10
|
||||||
|
if [ "${attempt}" -eq 3 ]; then
|
||||||
|
echo "Failed after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -466,7 +474,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -474,7 +482,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -511,7 +519,7 @@ jobs:
|
|||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
|||||||
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -40,9 +40,9 @@ env:
|
|||||||
CACHE_VERSION: 2
|
CACHE_VERSION: 2
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.12"
|
HA_SHORT_VERSION: "2026.1"
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: "3.13.11"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
|
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Check out code from GitHub
|
name: Check out code from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@@ -263,7 +263,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: &key-pre-commit-venv >-
|
key: &key-pre-commit-venv >-
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
- &cache-restore-pre-commit-venv
|
- &cache-restore-pre-commit-venv
|
||||||
name: Restore base Python virtual environment
|
name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -511,7 +511,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||||
with:
|
with:
|
||||||
path: *path-apt-cache
|
path: *path-apt-cache
|
||||||
key: *key-apt-cache
|
key: *key-apt-cache
|
||||||
@@ -534,7 +534,7 @@ jobs:
|
|||||||
python --version
|
python --version
|
||||||
uv pip freeze >> pip_freeze.txt
|
uv pip freeze >> pip_freeze.txt
|
||||||
- name: Upload pip_freeze artifact
|
- name: Upload pip_freeze artifact
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: pip-freeze-${{ matrix.python-version }}
|
name: pip-freeze-${{ matrix.python-version }}
|
||||||
path: pip_freeze.txt
|
path: pip_freeze.txt
|
||||||
@@ -864,7 +864,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- &compile-english-translations
|
- &compile-english-translations
|
||||||
@@ -1188,7 +1188,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1313,7 +1313,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
|||||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
|
|||||||
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Checkout the repository
|
name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
|
|
||||||
- &download-env-file
|
- &download-env-file
|
||||||
name: Download env_file
|
name: Download env_file
|
||||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@6066c17a2a4aafcf7bdfeae01717f63adfcdba98 # 2025.11.0
|
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
|||||||
26
CODEOWNERS
generated
26
CODEOWNERS
generated
@@ -73,6 +73,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airobot/ @mettolen
|
/tests/components/airobot/ @mettolen
|
||||||
/homeassistant/components/airos/ @CoMPaTech
|
/homeassistant/components/airos/ @CoMPaTech
|
||||||
/tests/components/airos/ @CoMPaTech
|
/tests/components/airos/ @CoMPaTech
|
||||||
|
/homeassistant/components/airpatrol/ @antondalgren
|
||||||
|
/tests/components/airpatrol/ @antondalgren
|
||||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||||
/tests/components/airq/ @Sibgatulin @dl2080
|
/tests/components/airq/ @Sibgatulin @dl2080
|
||||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||||
@@ -218,8 +220,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||||
/tests/components/blebox/ @bbx-a @swistakm
|
/tests/components/blebox/ @bbx-a @swistakm
|
||||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
/homeassistant/components/blink/ @fronzbot
|
||||||
/tests/components/blink/ @fronzbot @mkmer
|
/tests/components/blink/ @fronzbot
|
||||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/homeassistant/components/bluemaestro/ @bdraco
|
/homeassistant/components/bluemaestro/ @bdraco
|
||||||
@@ -306,8 +308,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/config/ @home-assistant/core
|
/tests/components/config/ @home-assistant/core
|
||||||
/homeassistant/components/configurator/ @home-assistant/core
|
/homeassistant/components/configurator/ @home-assistant/core
|
||||||
/tests/components/configurator/ @home-assistant/core
|
/tests/components/configurator/ @home-assistant/core
|
||||||
/homeassistant/components/control4/ @lawtancool
|
/homeassistant/components/control4/ @lawtancool @davidrecordon
|
||||||
/tests/components/control4/ @lawtancool
|
/tests/components/control4/ @lawtancool @davidrecordon
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/cookidoo/ @miaucl
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
@@ -418,6 +420,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
/tests/components/efergy/ @tkdrob
|
/tests/components/efergy/ @tkdrob
|
||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
|
/homeassistant/components/egauge/ @neggert
|
||||||
|
/tests/components/egauge/ @neggert
|
||||||
/homeassistant/components/eheimdigital/ @autinerd
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
/tests/components/eheimdigital/ @autinerd
|
/tests/components/eheimdigital/ @autinerd
|
||||||
/homeassistant/components/ekeybionyx/ @richardpolzer
|
/homeassistant/components/ekeybionyx/ @richardpolzer
|
||||||
@@ -460,7 +464,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom
|
||||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||||
@@ -539,6 +543,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
|
/homeassistant/components/fressnapf_tracker/ @eifinger
|
||||||
|
/tests/components/fressnapf_tracker/ @eifinger
|
||||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||||
@@ -569,6 +575,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/generic_hygrostat/ @Shulyaka
|
/tests/components/generic_hygrostat/ @Shulyaka
|
||||||
/homeassistant/components/geniushub/ @manzanotti
|
/homeassistant/components/geniushub/ @manzanotti
|
||||||
/tests/components/geniushub/ @manzanotti
|
/tests/components/geniushub/ @manzanotti
|
||||||
|
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
||||||
|
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
||||||
/homeassistant/components/geo_json_events/ @exxamalte
|
/homeassistant/components/geo_json_events/ @exxamalte
|
||||||
/tests/components/geo_json_events/ @exxamalte
|
/tests/components/geo_json_events/ @exxamalte
|
||||||
/homeassistant/components/geo_location/ @home-assistant/core
|
/homeassistant/components/geo_location/ @home-assistant/core
|
||||||
@@ -657,6 +665,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/here_travel_time/ @eifinger
|
/homeassistant/components/here_travel_time/ @eifinger
|
||||||
/tests/components/here_travel_time/ @eifinger
|
/tests/components/here_travel_time/ @eifinger
|
||||||
/homeassistant/components/hikvision/ @mezz64
|
/homeassistant/components/hikvision/ @mezz64
|
||||||
|
/tests/components/hikvision/ @mezz64
|
||||||
/homeassistant/components/hikvisioncam/ @fbradyirl
|
/homeassistant/components/hikvisioncam/ @fbradyirl
|
||||||
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
||||||
/tests/components/hisense_aehw4a1/ @bannhead
|
/tests/components/hisense_aehw4a1/ @bannhead
|
||||||
@@ -1354,8 +1363,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ring/ @sdb9696
|
/tests/components/ring/ @sdb9696
|
||||||
/homeassistant/components/risco/ @OnFreund
|
/homeassistant/components/risco/ @OnFreund
|
||||||
/tests/components/risco/ @OnFreund
|
/tests/components/risco/ @OnFreund
|
||||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
|
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
||||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
||||||
/homeassistant/components/rmvtransport/ @cgtobi
|
/homeassistant/components/rmvtransport/ @cgtobi
|
||||||
/tests/components/rmvtransport/ @cgtobi
|
/tests/components/rmvtransport/ @cgtobi
|
||||||
/homeassistant/components/roborock/ @Lash-L @allenporter
|
/homeassistant/components/roborock/ @Lash-L @allenporter
|
||||||
@@ -1761,6 +1770,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
/tests/components/vilfo/ @ManneW
|
/tests/components/vilfo/ @ManneW
|
||||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||||
|
/tests/components/vivotek/ @HarlemSquirrel
|
||||||
/homeassistant/components/vizio/ @raman325
|
/homeassistant/components/vizio/ @raman325
|
||||||
/tests/components/vizio/ @raman325
|
/tests/components/vizio/ @raman325
|
||||||
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
@@ -1800,6 +1810,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/weatherflow_cloud/ @jeeftor
|
/tests/components/weatherflow_cloud/ @jeeftor
|
||||||
/homeassistant/components/weatherkit/ @tjhorner
|
/homeassistant/components/weatherkit/ @tjhorner
|
||||||
/tests/components/weatherkit/ @tjhorner
|
/tests/components/weatherkit/ @tjhorner
|
||||||
|
/homeassistant/components/web_rtc/ @home-assistant/core
|
||||||
|
/tests/components/web_rtc/ @home-assistant/core
|
||||||
/homeassistant/components/webdav/ @jpbede
|
/homeassistant/components/webdav/ @jpbede
|
||||||
/tests/components/webdav/ @jpbede
|
/tests/components/webdav/ @jpbede
|
||||||
/homeassistant/components/webhook/ @home-assistant/core
|
/homeassistant/components/webhook/ @home-assistant/core
|
||||||
|
|||||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -30,7 +30,7 @@ RUN \
|
|||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
go2rtc --version \
|
go2rtc --version \
|
||||||
# Install uv
|
# Install uv
|
||||||
&& pip3 install uv==0.9.6
|
&& pip3 install uv==0.9.17
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
@@ -35,25 +35,22 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
|
|
||||||
COPY .python-version ./
|
|
||||||
RUN uv python install
|
|
||||||
|
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
RUN uv venv $VIRTUAL_ENV
|
RUN --mount=type=bind,source=.python-version,target=.python-version \
|
||||||
|
uv python install \
|
||||||
|
&& uv venv $VIRTUAL_ENV
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||||
&& uv pip install -e ~/hass-release/
|
&& uv pip install -e ~/hass-release/
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
COPY requirements.txt ./
|
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
|
||||||
RUN uv pip install -r requirements.txt
|
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
|
||||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||||
RUN uv pip install -r requirements_test.txt
|
uv pip install -r requirements.txt -r requirements_test.txt
|
||||||
|
|
||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any, Final
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
EVENT_CORE_CONFIG_UPDATE,
|
||||||
|
EVENT_LABS_UPDATED,
|
||||||
EVENT_LOVELACE_UPDATED,
|
EVENT_LOVELACE_UPDATED,
|
||||||
EVENT_PANELS_UPDATED,
|
EVENT_PANELS_UPDATED,
|
||||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||||
@@ -45,6 +46,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
|||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
EVENT_THEMES_UPDATED,
|
EVENT_THEMES_UPDATED,
|
||||||
EVENT_LABEL_REGISTRY_UPDATED,
|
EVENT_LABEL_REGISTRY_UPDATED,
|
||||||
|
EVENT_LABS_UPDATED,
|
||||||
EVENT_CATEGORY_REGISTRY_UPDATED,
|
EVENT_CATEGORY_REGISTRY_UPDATED,
|
||||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,13 +624,16 @@ async def async_enable_logging(
|
|||||||
|
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
if "SUPERVISOR" in os.environ:
|
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
|
||||||
_LOGGER.info("Running in Supervisor, not logging to file")
|
|
||||||
# Rename the default log file if it exists, since previous versions created
|
# Rename the default log file if it exists, since previous versions created
|
||||||
# it even on Supervisor
|
# it even on Supervisor
|
||||||
if os.path.isfile(default_log_path):
|
def rename_old_file() -> None:
|
||||||
with contextlib.suppress(OSError):
|
"""Rename old log file in executor."""
|
||||||
os.rename(default_log_path, f"{default_log_path}.old")
|
if os.path.isfile(default_log_path):
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
os.rename(default_log_path, f"{default_log_path}.old")
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(rename_old_file)
|
||||||
err_log_path = None
|
err_log_path = None
|
||||||
else:
|
else:
|
||||||
err_log_path = default_log_path
|
err_log_path = default_log_path
|
||||||
@@ -1000,7 +1003,7 @@ class _WatchPendingSetups:
|
|||||||
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
||||||
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Waiting on integrations to complete setup: %s",
|
"Waiting for integrations to complete setup: %s",
|
||||||
self._setup_started,
|
self._setup_started,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ from actron_neo_api import (
|
|||||||
|
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import _LOGGER
|
from .const import _LOGGER, DOMAIN
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
ActronAirConfigEntry,
|
ActronAirConfigEntry,
|
||||||
ActronAirRuntimeData,
|
ActronAirRuntimeData,
|
||||||
@@ -29,12 +30,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
|||||||
try:
|
try:
|
||||||
systems = await api.get_ac_systems()
|
systems = await api.get_ac_systems()
|
||||||
await api.update_status()
|
await api.update_status()
|
||||||
except ActronAirAuthError:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
raise ConfigEntryAuthFailed(
|
||||||
raise
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
) from err
|
||||||
except ActronAirAPIError as err:
|
except ActronAirAPIError as err:
|
||||||
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
raise ConfigEntryNotReady from err
|
||||||
raise
|
|
||||||
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||||
for system in systems:
|
for system in systems:
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Setup config flow for Actron Air integration."""
|
"""Setup config flow for Actron Air integration."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
@@ -95,8 +96,16 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
unique_id = str(user_data["id"])
|
unique_id = str(user_data["id"])
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
|
# Check if this is a reauth flow
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(),
|
||||||
|
data_updates={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_data["email"],
|
title=user_data["email"],
|
||||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||||
@@ -114,6 +123,21 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
del self.login_task
|
del self.login_task
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauthentication request."""
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm reauth dialog."""
|
||||||
|
if user_input is not None:
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="reauth_confirm")
|
||||||
|
|
||||||
async def async_step_connection_error(
|
async def async_step_connection_error(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -5,16 +5,23 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
|
from actron_neo_api import (
|
||||||
|
ActronAirACSystem,
|
||||||
|
ActronAirAPI,
|
||||||
|
ActronAirAuthError,
|
||||||
|
ActronAirStatus,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import _LOGGER
|
from .const import _LOGGER, DOMAIN
|
||||||
|
|
||||||
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
STALE_DEVICE_TIMEOUT = timedelta(minutes=5)
|
||||||
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
||||||
ERROR_UNKNOWN = "unknown_error"
|
ERROR_UNKNOWN = "unknown_error"
|
||||||
|
|
||||||
@@ -29,9 +36,6 @@ class ActronAirRuntimeData:
|
|||||||
|
|
||||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
||||||
|
|
||||||
AUTH_ERROR_THRESHOLD = 3
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||||
"""System coordinator for Actron Air integration."""
|
"""System coordinator for Actron Air integration."""
|
||||||
@@ -59,7 +63,14 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
|||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirStatus:
|
async def _async_update_data(self) -> ActronAirStatus:
|
||||||
"""Fetch updates and merge incremental changes into the full state."""
|
"""Fetch updates and merge incremental changes into the full state."""
|
||||||
await self.api.update_status()
|
try:
|
||||||
|
await self.api.update_status()
|
||||||
|
except ActronAirAuthError as err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
) from err
|
||||||
|
|
||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
return self.status
|
return self.status
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["actron-neo-api==0.1.87"]
|
"requirements": ["actron-neo-api==0.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: todo
|
reauthentication-flow: done
|
||||||
test-coverage: todo
|
test-coverage: todo
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
"oauth2_error": "Failed to start OAuth2 flow"
|
"oauth2_error": "Failed to start authentication flow",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"wrong_account": "You must reauthenticate with the same Actron Air account that was originally configured."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
|
"oauth2_error": "Failed to start authentication flow. Please try again later."
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
|
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
|
||||||
@@ -16,14 +18,23 @@
|
|||||||
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
||||||
"title": "Connection error"
|
"title": "Connection error"
|
||||||
},
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Your Actron Air authentication has expired. Select continue to reauthenticate with your Actron Air account. You will be prompted to log in again to restore the connection.",
|
||||||
|
"title": "Authentication expired"
|
||||||
|
},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"data": {},
|
"data": {},
|
||||||
"description": "The authorization process timed out. Please try again.",
|
"description": "The authentication process timed out. Please try again.",
|
||||||
"title": "Authorization timeout"
|
"title": "Authentication timeout"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Actron Air OAuth2 Authorization"
|
"title": "Actron Air Authentication"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"auth_error": {
|
||||||
|
"message": "Authentication failed, please reauthenticate"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@Bre77"],
|
"codeowners": ["@Bre77"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["advantage_air"],
|
"loggers": ["advantage_air"],
|
||||||
"requirements": ["advantage-air==0.4.4"]
|
"requirements": ["advantage-air==0.4.4"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyaftership==21.11.0"]
|
"requirements": ["pyaftership==21.11.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@ispysoftware"],
|
"codeowners": ["@ispysoftware"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["agent"],
|
"loggers": ["agent"],
|
||||||
"requirements": ["agent-py==0.0.24"]
|
"requirements": ["agent-py==0.0.24"]
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
||||||
_validate_structure_fields,
|
_validate_structure_fields,
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
||||||
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
{"accept": ["*/*"], "multiple": True}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
||||||
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
{"accept": ["*/*"], "multiple": True}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@asymworks"],
|
"codeowners": ["@asymworks"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyairnow"],
|
"loggers": ["pyairnow"],
|
||||||
"requirements": ["pyairnow==1.3.1"]
|
"requirements": ["pyairnow==1.3.1"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
|
|
||||||
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -174,6 +175,56 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
|
|||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauthentication upon an API authentication error."""
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm reauthentication dialog."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Combine existing data with new password
|
||||||
|
data = {
|
||||||
|
CONF_HOST: reauth_entry.data[CONF_HOST],
|
||||||
|
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
||||||
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
await validate_input(self.hass, data)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
reauth_entry,
|
||||||
|
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={
|
||||||
|
"username": reauth_entry.data[CONF_USERNAME],
|
||||||
|
"host": reauth_entry.data[CONF_HOST],
|
||||||
|
},
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@@ -53,7 +54,15 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
|
|||||||
try:
|
try:
|
||||||
status = await self.client.get_statuses()
|
status = await self.client.get_statuses()
|
||||||
settings = await self.client.get_settings()
|
settings = await self.client.get_settings()
|
||||||
except (AirobotAuthError, AirobotConnectionError) as err:
|
except AirobotAuthError as err:
|
||||||
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="authentication_failed",
|
||||||
|
) from err
|
||||||
|
except AirobotConnectionError as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="connection_failed",
|
||||||
|
) from err
|
||||||
|
|
||||||
return AirobotData(status=status, settings=settings)
|
return AirobotData(status=status, settings=settings)
|
||||||
|
|||||||
38
homeassistant/components/airobot/diagnostics.py
Normal file
38
homeassistant/components/airobot/diagnostics.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""Diagnostics support for Airobot."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AirobotConfigEntry
|
||||||
|
|
||||||
|
TO_REDACT_CONFIG = [CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: AirobotConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
# Build device capabilities info
|
||||||
|
device_capabilities = None
|
||||||
|
if coordinator.data:
|
||||||
|
device_capabilities = {
|
||||||
|
"has_floor_sensor": coordinator.data.status.has_floor_sensor,
|
||||||
|
"has_co2_sensor": coordinator.data.status.has_co2_sensor,
|
||||||
|
"hw_version": coordinator.data.status.hw_version,
|
||||||
|
"fw_version": coordinator.data.status.fw_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entry_data": async_redact_data(entry.data, TO_REDACT_CONFIG),
|
||||||
|
"device_capabilities": device_capabilities,
|
||||||
|
"status": asdict(coordinator.data.status) if coordinator.data else None,
|
||||||
|
"settings": asdict(coordinator.data.settings) if coordinator.data else None,
|
||||||
|
}
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyairobotrest"],
|
"loggers": ["pyairobotrest"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "silver",
|
||||||
"requirements": ["pyairobotrest==0.1.0"]
|
"requirements": ["pyairobotrest==0.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: todo
|
reauthentication-flow: done
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: todo
|
diagnostics: done
|
||||||
discovery-update-info: done
|
discovery-update-info: done
|
||||||
discovery: done
|
discovery: done
|
||||||
docs-data-update: done
|
docs-data-update: done
|
||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: done
|
||||||
docs-supported-devices: done
|
docs-supported-devices: done
|
||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
@@ -54,8 +54,8 @@ rules:
|
|||||||
comment: Single device integration, no dynamic device discovery needed.
|
comment: Single device integration, no dynamic device discovery needed.
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: todo
|
entity-disabled-by-default: done
|
||||||
entity-translations: todo
|
entity-translations: done
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations: todo
|
icon-translations: todo
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
|
|||||||
150
homeassistant/components/airobot/sensor.py
Normal file
150
homeassistant/components/airobot/sensor.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Sensor platform for Airobot thermostat."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from pyairobotrest.models import ThermostatStatus
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
PERCENTAGE,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfTemperature,
|
||||||
|
UnitOfTime,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
from homeassistant.util.variance import ignore_variance
|
||||||
|
|
||||||
|
from . import AirobotConfigEntry
|
||||||
|
from .entity import AirobotEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirobotSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Airobot sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[ThermostatStatus], StateType | datetime]
|
||||||
|
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
|
||||||
|
|
||||||
|
|
||||||
|
uptime_to_stable_datetime = ignore_variance(
|
||||||
|
lambda value: utcnow().replace(microsecond=0) - timedelta(seconds=value),
|
||||||
|
timedelta(minutes=2),
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="air_temperature",
|
||||||
|
translation_key="air_temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda status: status.temp_air,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="humidity",
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda status: status.hum_air,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="floor_temperature",
|
||||||
|
translation_key="floor_temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda status: status.temp_floor,
|
||||||
|
supported_fn=lambda status: status.has_floor_sensor,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="co2",
|
||||||
|
device_class=SensorDeviceClass.CO2,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda status: status.co2,
|
||||||
|
supported_fn=lambda status: status.has_co2_sensor,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="air_quality_index",
|
||||||
|
device_class=SensorDeviceClass.AQI,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda status: status.aqi,
|
||||||
|
supported_fn=lambda status: status.has_co2_sensor,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="heating_uptime",
|
||||||
|
translation_key="heating_uptime",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda status: status.heating_uptime,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="errors",
|
||||||
|
translation_key="errors",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda status: status.errors,
|
||||||
|
),
|
||||||
|
AirobotSensorEntityDescription(
|
||||||
|
key="device_uptime",
|
||||||
|
translation_key="device_uptime",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda status: uptime_to_stable_datetime(status.device_uptime),
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirobotConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Airobot sensor platform."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
AirobotSensor(coordinator, description)
|
||||||
|
for description in SENSOR_TYPES
|
||||||
|
if description.supported_fn(coordinator.data.status)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirobotSensor(AirobotEntity, SensorEntity):
|
||||||
|
"""Representation of an Airobot sensor."""
|
||||||
|
|
||||||
|
entity_description: AirobotSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator,
|
||||||
|
description: AirobotSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType | datetime:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data.status)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@@ -14,15 +15,24 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "The thermostat password."
|
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
||||||
},
|
},
|
||||||
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
||||||
},
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
||||||
|
},
|
||||||
|
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]"
|
"username": "Device ID"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your Airobot thermostat.",
|
"host": "The hostname or IP address of your Airobot thermostat.",
|
||||||
@@ -33,7 +43,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"air_temperature": {
|
||||||
|
"name": "Air temperature"
|
||||||
|
},
|
||||||
|
"device_uptime": {
|
||||||
|
"name": "Device uptime"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"name": "Error count"
|
||||||
|
},
|
||||||
|
"floor_temperature": {
|
||||||
|
"name": "Floor temperature"
|
||||||
|
},
|
||||||
|
"heating_uptime": {
|
||||||
|
"name": "Heating uptime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"authentication_failed": {
|
||||||
|
"message": "Authentication failed, please reauthenticate."
|
||||||
|
},
|
||||||
|
"connection_failed": {
|
||||||
|
"message": "Failed to communicate with device."
|
||||||
|
},
|
||||||
"set_preset_mode_failed": {
|
"set_preset_mode_failed": {
|
||||||
"message": "Failed to set preset mode to {preset_mode}."
|
"message": "Failed to set preset mode to {preset_mode}."
|
||||||
},
|
},
|
||||||
|
|||||||
24
homeassistant/components/airpatrol/__init__.py
Normal file
24
homeassistant/components/airpatrol/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""The AirPatrol integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import PLATFORMS
|
||||||
|
from .coordinator import AirPatrolConfigEntry, AirPatrolDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
||||||
|
"""Set up AirPatrol from a config entry."""
|
||||||
|
coordinator = AirPatrolDataUpdateCoordinator(hass, entry)
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
208
homeassistant/components/airpatrol/climate.py
Normal file
208
homeassistant/components/airpatrol/climate.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""Climate platform for AirPatrol integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
FAN_AUTO,
|
||||||
|
FAN_HIGH,
|
||||||
|
FAN_LOW,
|
||||||
|
SWING_OFF,
|
||||||
|
SWING_ON,
|
||||||
|
ClimateEntity,
|
||||||
|
ClimateEntityFeature,
|
||||||
|
HVACMode,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirPatrolConfigEntry
|
||||||
|
from .coordinator import AirPatrolDataUpdateCoordinator
|
||||||
|
from .entity import AirPatrolEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
AP_TO_HA_HVAC_MODES = {
|
||||||
|
"heat": HVACMode.HEAT,
|
||||||
|
"cool": HVACMode.COOL,
|
||||||
|
"off": HVACMode.OFF,
|
||||||
|
}
|
||||||
|
HA_TO_AP_HVAC_MODES = {value: key for key, value in AP_TO_HA_HVAC_MODES.items()}
|
||||||
|
|
||||||
|
AP_TO_HA_FAN_MODES = {
|
||||||
|
"min": FAN_LOW,
|
||||||
|
"max": FAN_HIGH,
|
||||||
|
"auto": FAN_AUTO,
|
||||||
|
}
|
||||||
|
HA_TO_AP_FAN_MODES = {value: key for key, value in AP_TO_HA_FAN_MODES.items()}
|
||||||
|
|
||||||
|
AP_TO_HA_SWING_MODES = {
|
||||||
|
"on": SWING_ON,
|
||||||
|
"off": SWING_OFF,
|
||||||
|
}
|
||||||
|
HA_TO_AP_SWING_MODES = {value: key for key, value in AP_TO_HA_SWING_MODES.items()}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirPatrolConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirPatrol climate entities."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
units = coordinator.data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AirPatrolClimate(coordinator, unit_id)
|
||||||
|
for unit_id, unit in units.items()
|
||||||
|
if "climate" in unit
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
|
||||||
|
"""AirPatrol climate entity."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.FAN_MODE
|
||||||
|
| ClimateEntityFeature.SWING_MODE
|
||||||
|
| ClimateEntityFeature.TURN_OFF
|
||||||
|
| ClimateEntityFeature.TURN_ON
|
||||||
|
)
|
||||||
|
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
||||||
|
_attr_fan_modes = [FAN_LOW, FAN_HIGH, FAN_AUTO]
|
||||||
|
_attr_swing_modes = [SWING_ON, SWING_OFF]
|
||||||
|
_attr_min_temp = 16.0
|
||||||
|
_attr_max_temp = 30.0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirPatrolDataUpdateCoordinator,
|
||||||
|
unit_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the climate entity."""
|
||||||
|
super().__init__(coordinator, unit_id)
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def climate_data(self) -> dict[str, Any]:
|
||||||
|
"""Return the climate data."""
|
||||||
|
return self.device_data.get("climate") or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params(self) -> dict[str, Any]:
|
||||||
|
"""Return the current parameters for the climate entity."""
|
||||||
|
return self.climate_data.get("ParametersData") or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return super().available and bool(self.climate_data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_humidity(self) -> float | None:
|
||||||
|
"""Return the current humidity."""
|
||||||
|
if humidity := self.climate_data.get("RoomHumidity"):
|
||||||
|
return float(humidity)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> float | None:
|
||||||
|
"""Return the current temperature."""
|
||||||
|
if temp := self.climate_data.get("RoomTemp"):
|
||||||
|
return float(temp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> float | None:
|
||||||
|
"""Return the target temperature."""
|
||||||
|
if temp := self.params.get("PumpTemp"):
|
||||||
|
return float(temp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_mode(self) -> HVACMode | None:
|
||||||
|
"""Return the current HVAC mode."""
|
||||||
|
pump_power = self.params.get("PumpPower")
|
||||||
|
pump_mode = self.params.get("PumpMode")
|
||||||
|
|
||||||
|
if pump_power and pump_power == "on" and pump_mode:
|
||||||
|
return AP_TO_HA_HVAC_MODES.get(pump_mode)
|
||||||
|
return HVACMode.OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_mode(self) -> str | None:
|
||||||
|
"""Return the current fan mode."""
|
||||||
|
fan_speed = self.params.get("FanSpeed")
|
||||||
|
if fan_speed:
|
||||||
|
return AP_TO_HA_FAN_MODES.get(fan_speed)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def swing_mode(self) -> str | None:
|
||||||
|
"""Return the current swing mode."""
|
||||||
|
swing = self.params.get("Swing")
|
||||||
|
if swing:
|
||||||
|
return AP_TO_HA_SWING_MODES.get(swing)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
params = self.params.copy()
|
||||||
|
|
||||||
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
|
temp = kwargs[ATTR_TEMPERATURE]
|
||||||
|
params["PumpTemp"] = f"{temp:.3f}"
|
||||||
|
|
||||||
|
await self._async_set_params(params)
|
||||||
|
|
||||||
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
|
"""Set new target hvac mode."""
|
||||||
|
params = self.params.copy()
|
||||||
|
|
||||||
|
if hvac_mode == HVACMode.OFF:
|
||||||
|
params["PumpPower"] = "off"
|
||||||
|
else:
|
||||||
|
params["PumpPower"] = "on"
|
||||||
|
params["PumpMode"] = HA_TO_AP_HVAC_MODES.get(hvac_mode)
|
||||||
|
|
||||||
|
await self._async_set_params(params)
|
||||||
|
|
||||||
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
|
"""Set new target fan mode."""
|
||||||
|
params = self.params.copy()
|
||||||
|
params["FanSpeed"] = HA_TO_AP_FAN_MODES.get(fan_mode)
|
||||||
|
|
||||||
|
await self._async_set_params(params)
|
||||||
|
|
||||||
|
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||||
|
"""Set new target swing mode."""
|
||||||
|
params = self.params.copy()
|
||||||
|
params["Swing"] = HA_TO_AP_SWING_MODES.get(swing_mode)
|
||||||
|
|
||||||
|
await self._async_set_params(params)
|
||||||
|
|
||||||
|
async def async_turn_on(self) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
params = self.params.copy()
|
||||||
|
if mode := AP_TO_HA_HVAC_MODES.get(params["PumpMode"]):
|
||||||
|
await self.async_set_hvac_mode(mode)
|
||||||
|
|
||||||
|
async def async_turn_off(self) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
|
|
||||||
|
async def _async_set_params(self, params: dict[str, Any]) -> None:
|
||||||
|
"""Set the unit to dry mode."""
|
||||||
|
new_climate_data = self.climate_data.copy()
|
||||||
|
new_climate_data["ParametersData"] = params
|
||||||
|
|
||||||
|
await self.coordinator.api.set_unit_climate_data(
|
||||||
|
self._unit_id, new_climate_data
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
111
homeassistant/components/airpatrol/config_flow.py
Normal file
111
homeassistant/components/airpatrol/config_flow.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""Config flow for the AirPatrol integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
TextSelectorType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_EMAIL): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.EMAIL,
|
||||||
|
autocomplete="email",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_PASSWORD): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.PASSWORD,
|
||||||
|
autocomplete="current-password",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_api(
|
||||||
|
hass: HomeAssistant, user_input: dict[str, str]
|
||||||
|
) -> tuple[str | None, str | None, dict[str, str]]:
|
||||||
|
"""Validate the API connection."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
access_token = None
|
||||||
|
unique_id = None
|
||||||
|
try:
|
||||||
|
api = await AirPatrolAPI.authenticate(
|
||||||
|
session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
|
)
|
||||||
|
except AirPatrolAuthenticationError:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except AirPatrolError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
access_token = api.get_access_token()
|
||||||
|
unique_id = api.get_unique_id()
|
||||||
|
|
||||||
|
return (access_token, unique_id, errors)
|
||||||
|
|
||||||
|
|
||||||
|
class AirPatrolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for AirPatrol."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
||||||
|
if access_token and unique_id:
|
||||||
|
user_input[CONF_ACCESS_TOKEN] = access_token
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_EMAIL], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, user_input: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauthentication with new credentials."""
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauthentication confirmation."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input:
|
||||||
|
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
||||||
|
if access_token and unique_id:
|
||||||
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
user_input[CONF_ACCESS_TOKEN] = access_token
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(), data_updates=user_input
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
16
homeassistant/components/airpatrol/const.py
Normal file
16
homeassistant/components/airpatrol/const.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""Constants for the AirPatrol integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airpatrol.api import AirPatrolAuthenticationError, AirPatrolError
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
|
DOMAIN = "airpatrol"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
PLATFORMS = [Platform.CLIMATE]
|
||||||
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
|
||||||
100
homeassistant/components/airpatrol/coordinator.py
Normal file
100
homeassistant/components/airpatrol/coordinator.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""Data update coordinator for AirPatrol."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||||
|
|
||||||
|
type AirPatrolConfigEntry = ConfigEntry[AirPatrolDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirPatrolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||||
|
"""Class to manage fetching AirPatrol data."""
|
||||||
|
|
||||||
|
config_entry: AirPatrolConfigEntry
|
||||||
|
api: AirPatrolAPI
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config_entry: AirPatrolConfigEntry) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name=f"{DOMAIN.capitalize()} {config_entry.title}",
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
try:
|
||||||
|
await self._setup_client()
|
||||||
|
except AirPatrolError as api_err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Error communicating with AirPatrol API: {api_err}"
|
||||||
|
) from api_err
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Update unit data from AirPatrol API."""
|
||||||
|
return {unit_data["unit_id"]: unit_data for unit_data in await self._get_data()}
|
||||||
|
|
||||||
|
async def _get_data(self, retry: bool = False) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch data from API."""
|
||||||
|
try:
|
||||||
|
return await self.api.get_data()
|
||||||
|
except AirPatrolAuthenticationError as auth_err:
|
||||||
|
if retry:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
"Authentication with AirPatrol failed"
|
||||||
|
) from auth_err
|
||||||
|
await self._update_token()
|
||||||
|
return await self._get_data(retry=True)
|
||||||
|
except AirPatrolError as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Error communicating with AirPatrol API: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
async def _update_token(self) -> None:
|
||||||
|
"""Refresh the AirPatrol API client and update the access token."""
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
try:
|
||||||
|
self.api = await AirPatrolAPI.authenticate(
|
||||||
|
session,
|
||||||
|
self.config_entry.data[CONF_EMAIL],
|
||||||
|
self.config_entry.data[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
except AirPatrolAuthenticationError as auth_err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
"Authentication with AirPatrol failed"
|
||||||
|
) from auth_err
|
||||||
|
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self.config_entry,
|
||||||
|
data={
|
||||||
|
**self.config_entry.data,
|
||||||
|
CONF_ACCESS_TOKEN: self.api.get_access_token(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _setup_client(self) -> None:
|
||||||
|
"""Set up the AirPatrol API client from stored access_token."""
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
api = AirPatrolAPI(
|
||||||
|
session,
|
||||||
|
self.config_entry.data[CONF_ACCESS_TOKEN],
|
||||||
|
self.config_entry.unique_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await api.get_data()
|
||||||
|
except AirPatrolAuthenticationError:
|
||||||
|
await self._update_token()
|
||||||
|
self.api = api
|
||||||
44
homeassistant/components/airpatrol/entity.py
Normal file
44
homeassistant/components/airpatrol/entity.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Base entity for AirPatrol integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirPatrolDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
|
||||||
|
"""Base entity for AirPatrol devices."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirPatrolDataUpdateCoordinator,
|
||||||
|
unit_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the AirPatrol entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._unit_id = unit_id
|
||||||
|
device = coordinator.data[unit_id]
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, unit_id)},
|
||||||
|
name=device["name"],
|
||||||
|
manufacturer=device["manufacturer"],
|
||||||
|
model=device["model"],
|
||||||
|
serial_number=device["hwid"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_data(self) -> dict[str, Any]:
|
||||||
|
"""Return the device data."""
|
||||||
|
return self.coordinator.data[self._unit_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return super().available and self._unit_id in self.coordinator.data
|
||||||
11
homeassistant/components/airpatrol/manifest.json
Normal file
11
homeassistant/components/airpatrol/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"domain": "airpatrol",
|
||||||
|
"name": "AirPatrol",
|
||||||
|
"codeowners": ["@antondalgren"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/airpatrol",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["airpatrol==0.1.0"]
|
||||||
|
}
|
||||||
65
homeassistant/components/airpatrol/quality_scale.yaml
Normal file
65
homeassistant/components/airpatrol/quality_scale.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup: done
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not provide custom actions
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities doesn't subscribe to events.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: done
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: todo
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
||||||
38
homeassistant/components/airpatrol/strings.json
Normal file
38
homeassistant/components/airpatrol/strings.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"unique_id_mismatch": "Login credentials do not match the configured account"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "[%key:component::airpatrol::config::step::user::data_description::email%]",
|
||||||
|
"password": "[%key:component::airpatrol::config::step::user::data_description::password%]"
|
||||||
|
},
|
||||||
|
"description": "Reauthenticate with AirPatrol"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"email": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"email": "Your AirPatrol email address",
|
||||||
|
"password": "Your AirPatrol password"
|
||||||
|
},
|
||||||
|
"description": "Connect to AirPatrol"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["airthings"],
|
"loggers": ["airthings"],
|
||||||
"requirements": ["airthings-cloud==0.2.0"]
|
"requirements": ["airthings-cloud==0.2.0"]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==1.2.0"]
|
"requirements": ["airthings-ble==1.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@samsinnamon"],
|
"codeowners": ["@samsinnamon"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["airtouch4pyapi"],
|
"loggers": ["airtouch4pyapi"],
|
||||||
"requirements": ["airtouch4pyapi==1.0.5"]
|
"requirements": ["airtouch4pyapi==1.0.5"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@danzel"],
|
"codeowners": ["@danzel"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["airtouch5py"],
|
"loggers": ["airtouch5py"],
|
||||||
"requirements": ["airtouch5py==0.3.0"]
|
"requirements": ["airtouch5py==0.3.0"]
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==1.0.2"]
|
"requirements": ["aioairzone==1.0.4"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.7.2"]
|
"requirements": ["aioairzone-cloud==0.7.2"]
|
||||||
|
|||||||
@@ -159,74 +159,74 @@
|
|||||||
"title": "Alarm control panel",
|
"title": "Alarm control panel",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"armed": {
|
"armed": {
|
||||||
"description": "Triggers when an alarm is armed.",
|
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is armed"
|
"name": "Alarm armed"
|
||||||
},
|
},
|
||||||
"armed_away": {
|
"armed_away": {
|
||||||
"description": "Triggers when an alarm is armed away.",
|
"description": "Triggers after one or more alarms become armed in away mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is armed away"
|
"name": "Alarm armed away"
|
||||||
},
|
},
|
||||||
"armed_home": {
|
"armed_home": {
|
||||||
"description": "Triggers when an alarm is armed home.",
|
"description": "Triggers after one or more alarms become armed in home mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is armed home"
|
"name": "Alarm armed home"
|
||||||
},
|
},
|
||||||
"armed_night": {
|
"armed_night": {
|
||||||
"description": "Triggers when an alarm is armed night.",
|
"description": "Triggers after one or more alarms become armed in night mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is armed night"
|
"name": "Alarm armed night"
|
||||||
},
|
},
|
||||||
"armed_vacation": {
|
"armed_vacation": {
|
||||||
"description": "Triggers when an alarm is armed vacation.",
|
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is armed vacation"
|
"name": "Alarm armed vacation"
|
||||||
},
|
},
|
||||||
"disarmed": {
|
"disarmed": {
|
||||||
"description": "Triggers when an alarm is disarmed.",
|
"description": "Triggers after one or more alarms become disarmed.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is disarmed"
|
"name": "Alarm disarmed"
|
||||||
},
|
},
|
||||||
"triggered": {
|
"triggered": {
|
||||||
"description": "Triggers when an alarm is triggered.",
|
"description": "Triggers after one or more alarms become triggered.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an alarm is triggered"
|
"name": "Alarm triggered"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
EntityStateTriggerBase,
|
EntityTargetStateTriggerBase,
|
||||||
Trigger,
|
Trigger,
|
||||||
make_conditional_entity_state_trigger,
|
make_entity_target_state_trigger,
|
||||||
make_entity_state_trigger,
|
make_entity_transition_trigger,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||||
@@ -21,7 +21,7 @@ def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||||
"""Trigger for entity state changes."""
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
_required_features: int
|
_required_features: int
|
||||||
@@ -38,7 +38,7 @@ class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
|||||||
|
|
||||||
def make_entity_state_trigger_required_features(
|
def make_entity_state_trigger_required_features(
|
||||||
domain: str, to_state: str, required_features: int
|
domain: str, to_state: str, required_features: int
|
||||||
) -> type[EntityStateTriggerBase]:
|
) -> type[EntityTargetStateTriggerBase]:
|
||||||
"""Create an entity state trigger class."""
|
"""Create an entity state trigger class."""
|
||||||
|
|
||||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||||
@@ -52,7 +52,7 @@ def make_entity_state_trigger_required_features(
|
|||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
"armed": make_conditional_entity_state_trigger(
|
"armed": make_entity_transition_trigger(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
from_states={
|
from_states={
|
||||||
AlarmControlPanelState.ARMING,
|
AlarmControlPanelState.ARMING,
|
||||||
@@ -89,8 +89,12 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
|||||||
AlarmControlPanelState.ARMED_VACATION,
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||||
),
|
),
|
||||||
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
|
"disarmed": make_entity_target_state_trigger(
|
||||||
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
|
DOMAIN, AlarmControlPanelState.DISARMED
|
||||||
|
),
|
||||||
|
"triggered": make_entity_target_state_trigger(
|
||||||
|
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@madpilot"],
|
"codeowners": ["@madpilot"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["amberelectric"],
|
"loggers": ["amberelectric"],
|
||||||
"requirements": ["amberelectric==2.0.12"]
|
"requirements": ["amberelectric==2.0.12"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@engrbm87"],
|
"codeowners": ["@engrbm87"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ async def async_setup_entry(
|
|||||||
cookie_jar=CookieJar(quote_cookie=False),
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
),
|
),
|
||||||
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
||||||
account_number=entry.data[CONF_ACCOUNT_NUMBER],
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await auth.send_refresh_request()
|
await auth.send_refresh_request()
|
||||||
@@ -49,7 +48,7 @@ async def async_setup_entry(
|
|||||||
_aw = AnglianWater(authenticator=auth)
|
_aw = AnglianWater(authenticator=auth)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await _aw.validate_smart_meter()
|
await _aw.validate_smart_meter(entry.data[CONF_ACCOUNT_NUMBER])
|
||||||
except SmartMeterUnavailableError as err:
|
except SmartMeterUnavailableError as err:
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
|
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiohttp import CookieJar
|
from aiohttp import CookieJar
|
||||||
from pyanglianwater import AnglianWater
|
from pyanglianwater import AnglianWater
|
||||||
from pyanglianwater.auth import BaseAuth, MSOB2CAuth
|
from pyanglianwater.auth import MSOB2CAuth
|
||||||
from pyanglianwater.exceptions import (
|
from pyanglianwater.exceptions import (
|
||||||
InvalidAccountIdError,
|
InvalidAccountIdError,
|
||||||
SelfAssertedError,
|
SelfAssertedError,
|
||||||
@@ -30,11 +30,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
||||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||||
),
|
),
|
||||||
|
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
|
async def validate_credentials(
|
||||||
|
auth: MSOB2CAuth, account_number: str
|
||||||
|
) -> str | MSOB2CAuth:
|
||||||
"""Validate the provided credentials."""
|
"""Validate the provided credentials."""
|
||||||
try:
|
try:
|
||||||
await auth.send_login_request()
|
await auth.send_login_request()
|
||||||
@@ -45,7 +48,7 @@ async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
_aw = AnglianWater(authenticator=auth)
|
_aw = AnglianWater(authenticator=auth)
|
||||||
try:
|
try:
|
||||||
await _aw.validate_smart_meter()
|
await _aw.validate_smart_meter(account_number)
|
||||||
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
||||||
return "smart_meter_unavailable"
|
return "smart_meter_unavailable"
|
||||||
return auth
|
return auth
|
||||||
@@ -68,35 +71,21 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self.hass,
|
self.hass,
|
||||||
cookie_jar=CookieJar(quote_cookie=False),
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
),
|
),
|
||||||
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
|
),
|
||||||
)
|
user_input[CONF_ACCOUNT_NUMBER],
|
||||||
)
|
)
|
||||||
if isinstance(validation_response, BaseAuth):
|
if isinstance(validation_response, str):
|
||||||
account_number = (
|
errors["base"] = validation_response
|
||||||
user_input.get(CONF_ACCOUNT_NUMBER)
|
else:
|
||||||
or validation_response.account_number
|
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
|
||||||
)
|
|
||||||
await self.async_set_unique_id(account_number)
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=account_number,
|
title=user_input[CONF_ACCOUNT_NUMBER],
|
||||||
data={
|
data={
|
||||||
**user_input,
|
**user_input,
|
||||||
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
||||||
CONF_ACCOUNT_NUMBER: account_number,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if validation_response == "smart_meter_unavailable":
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors={"base": validation_response},
|
|
||||||
)
|
|
||||||
errors["base"] = validation_response
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
||||||
|
|
||||||
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
|
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
|
||||||
|
|
||||||
@@ -44,6 +44,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Update data from Anglian Water's API."""
|
"""Update data from Anglian Water's API."""
|
||||||
try:
|
try:
|
||||||
return await self.api.update()
|
return await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
|
||||||
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
|
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
|
||||||
raise UpdateFailed from err
|
raise UpdateFailed from err
|
||||||
|
|||||||
@@ -18,17 +18,21 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
|
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
|
||||||
"""Defines a Anglian Water entity."""
|
"""Defines a Anglian Water entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AnglianWaterUpdateCoordinator,
|
coordinator: AnglianWaterUpdateCoordinator,
|
||||||
smart_meter: SmartMeter,
|
smart_meter: SmartMeter,
|
||||||
|
key: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Anglian Water entity."""
|
"""Initialize Anglian Water entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.smart_meter = smart_meter
|
self.smart_meter = smart_meter
|
||||||
|
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, smart_meter.serial_number)},
|
identifiers={(DOMAIN, smart_meter.serial_number)},
|
||||||
name="Smart Water Meter",
|
name=smart_meter.serial_number,
|
||||||
manufacturer="Anglian Water",
|
manufacturer="Anglian Water",
|
||||||
serial_number=smart_meter.serial_number,
|
serial_number=smart_meter.serial_number,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
"codeowners": ["@pantherale0"],
|
"codeowners": ["@pantherale0"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["pyanglianwater"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyanglianwater==2.1.0"]
|
"requirements": ["pyanglianwater==3.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,9 +108,8 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
|
|||||||
description: AnglianWaterSensorEntityDescription,
|
description: AnglianWaterSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Anglian Water sensor."""
|
"""Initialize Anglian Water sensor."""
|
||||||
super().__init__(coordinator, smart_meter)
|
super().__init__(coordinator, smart_meter, description.key)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{smart_meter.serial_number}_{description.key}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | None:
|
def native_value(self) -> float | None:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@Lash-L"],
|
"codeowners": ["@Lash-L"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["anova_wifi"],
|
"loggers": ["anova_wifi"],
|
||||||
"requirements": ["anova-wifi==0.17.0"]
|
"requirements": ["anova-wifi==0.17.0"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@hyralex"],
|
"codeowners": ["@hyralex"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["anthemav"],
|
"loggers": ["anthemav"],
|
||||||
"requirements": ["anthemav==1.4.1"]
|
"requirements": ["anthemav==1.4.1"]
|
||||||
|
|||||||
@@ -421,6 +421,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
)
|
)
|
||||||
if short_form.search(model_alias):
|
if short_form.search(model_alias):
|
||||||
model_alias += "-0"
|
model_alias += "-0"
|
||||||
|
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
||||||
|
model_alias += "-latest"
|
||||||
model_options.append(
|
model_options.append(
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
label=model_info.display_name,
|
label=model_info.display_name,
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
name=subentry.title,
|
name=subentry.title,
|
||||||
manufacturer="Anthropic",
|
manufacturer="Anthropic",
|
||||||
model="Claude",
|
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
|
||||||
entry_type=dr.DeviceEntryType.SERVICE,
|
entry_type=dr.DeviceEntryType.SERVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["anthropic==0.73.0"]
|
"requirements": ["anthropic==0.75.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@bdr99"],
|
"codeowners": ["@bdr99"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.15"]
|
"requirements": ["py-aosmith==1.0.15"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@yuxincs"],
|
"codeowners": ["@yuxincs"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"],
|
"loggers": ["apcaccess"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["zeroconf"],
|
"dependencies": ["zeroconf"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyatv", "srptools"],
|
"loggers": ["pyatv", "srptools"],
|
||||||
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
|
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@elupus"],
|
"codeowners": ["@elupus"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["arcam"],
|
"loggers": ["arcam"],
|
||||||
"requirements": ["arcam-fmj==1.8.2"],
|
"requirements": ["arcam-fmj==1.8.2"],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@ikalnyi"],
|
"codeowners": ["@ikalnyi"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["asyncarve==0.1.1"]
|
"requirements": ["asyncarve==0.1.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@milanmeu"],
|
"codeowners": ["@milanmeu"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioaseko"],
|
"loggers": ["aioaseko"],
|
||||||
"requirements": ["aioaseko==1.0.0"]
|
"requirements": ["aioaseko==1.0.0"]
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
from pymicro_vad import MicroVad
|
from pysilero_vad import SileroVoiceActivityDetector
|
||||||
from pyspeex_noise import AudioProcessor
|
from pyspeex_noise import AudioProcessor
|
||||||
|
|
||||||
from .const import BYTES_PER_CHUNK
|
from .const import BYTES_PER_CHUNK
|
||||||
@@ -42,8 +43,8 @@ class AudioEnhancer(ABC):
|
|||||||
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
|
|
||||||
|
|
||||||
class MicroVadSpeexEnhancer(AudioEnhancer):
|
class SileroVadSpeexEnhancer(AudioEnhancer):
|
||||||
"""Audio enhancer that runs microVAD and speex."""
|
"""Audio enhancer that runs Silero VAD and speex."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||||
@@ -69,21 +70,49 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
|
|||||||
self.noise_suppression,
|
self.noise_suppression,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.vad: MicroVad | None = None
|
self.vad: SileroVoiceActivityDetector | None = None
|
||||||
|
|
||||||
|
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
|
||||||
|
# buffer audio. The previous speech probability is used until enough
|
||||||
|
# audio has been buffered.
|
||||||
|
self._vad_buffer: bytearray | None = None
|
||||||
|
self._vad_buffer_chunks = 0
|
||||||
|
self._vad_buffer_chunk_idx = 0
|
||||||
|
self._last_speech_probability: float | None = None
|
||||||
|
|
||||||
if self.is_vad_enabled:
|
if self.is_vad_enabled:
|
||||||
self.vad = MicroVad()
|
self.vad = SileroVoiceActivityDetector()
|
||||||
_LOGGER.debug("Initialized microVAD")
|
|
||||||
|
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
|
||||||
|
self._vad_buffer_chunks = int(
|
||||||
|
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
|
||||||
|
)
|
||||||
|
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
|
||||||
|
self._vad_buffer = bytearray(self.vad.chunk_bytes())
|
||||||
|
_LOGGER.debug("Initialized Silero VAD")
|
||||||
|
|
||||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
speech_probability: float | None = None
|
|
||||||
|
|
||||||
assert len(audio) == BYTES_PER_CHUNK
|
assert len(audio) == BYTES_PER_CHUNK
|
||||||
|
|
||||||
if self.vad is not None:
|
if self.vad is not None:
|
||||||
# Run VAD
|
# Run VAD
|
||||||
speech_probability = self.vad.Process10ms(audio)
|
assert self._vad_buffer is not None
|
||||||
|
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
|
||||||
|
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
|
||||||
|
|
||||||
|
self._vad_buffer_chunk_idx += 1
|
||||||
|
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
|
||||||
|
# We have enough data to run Silero VAD (32 ms)
|
||||||
|
self._last_speech_probability = self.vad.process_chunk(
|
||||||
|
self._vad_buffer[: self.vad.chunk_bytes()]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy leftover audio that wasn't processed to start
|
||||||
|
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
|
||||||
|
-self._vad_leftover_bytes :
|
||||||
|
]
|
||||||
|
self._vad_buffer_chunk_idx = 0
|
||||||
|
|
||||||
if self.audio_processor is not None:
|
if self.audio_processor is not None:
|
||||||
# Run noise suppression and auto gain
|
# Run noise suppression and auto gain
|
||||||
@@ -92,5 +121,5 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
|
|||||||
return EnhancedAudioChunk(
|
return EnhancedAudioChunk(
|
||||||
audio=audio,
|
audio=audio,
|
||||||
timestamp_ms=timestamp_ms,
|
timestamp_ms=timestamp_ms,
|
||||||
speech_probability=speech_probability,
|
speech_probability=self._last_speech_probability,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
|
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from homeassistant.util import (
|
|||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||||
|
|
||||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
|
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer
|
||||||
from .const import (
|
from .const import (
|
||||||
ACKNOWLEDGE_PATH,
|
ACKNOWLEDGE_PATH,
|
||||||
BYTES_PER_CHUNK,
|
BYTES_PER_CHUNK,
|
||||||
@@ -633,7 +633,7 @@ class PipelineRun:
|
|||||||
# Initialize with audio settings
|
# Initialize with audio settings
|
||||||
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
||||||
# Default audio enhancer
|
# Default audio enhancer
|
||||||
self.audio_enhancer = MicroVadSpeexEnhancer(
|
self.audio_enhancer = SileroVadSpeexEnhancer(
|
||||||
self.audio_settings.auto_gain_dbfs,
|
self.audio_settings.auto_gain_dbfs,
|
||||||
self.audio_settings.noise_suppression_level,
|
self.audio_settings.noise_suppression_level,
|
||||||
self.audio_settings.is_vad_enabled,
|
self.audio_settings.is_vad_enabled,
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||||
"integration_type": "entity",
|
"integration_type": "entity",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==3.4.0"]
|
"requirements": ["hassil==3.5.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,44 +112,44 @@
|
|||||||
"title": "Assist satellite",
|
"title": "Assist satellite",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"idle": {
|
"idle": {
|
||||||
"description": "Triggers when an Assist satellite becomes idle.",
|
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an Assist satellite becomes idle"
|
"name": "Satellite became idle"
|
||||||
},
|
},
|
||||||
"listening": {
|
"listening": {
|
||||||
"description": "Triggers when an Assist satellite starts listening.",
|
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an Assist satellite starts listening"
|
"name": "Satellite started listening"
|
||||||
},
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"description": "Triggers when an Assist satellite is processing.",
|
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an Assist satellite is processing"
|
"name": "Satellite started processing"
|
||||||
},
|
},
|
||||||
"responding": {
|
"responding": {
|
||||||
"description": "Triggers when an Assist satellite is responding.",
|
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "When an Assist satellite is responding"
|
"name": "Satellite started responding"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
"""Provides triggers for assist satellites."""
|
"""Provides triggers for assist satellites."""
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import AssistSatelliteState
|
from .entity import AssistSatelliteState
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
"idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
"idle": make_entity_target_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||||
"listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
|
"listening": make_entity_target_state_trigger(
|
||||||
"processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
|
DOMAIN, AssistSatelliteState.LISTENING
|
||||||
"responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
|
),
|
||||||
|
"processing": make_entity_target_state_trigger(
|
||||||
|
DOMAIN, AssistSatelliteState.PROCESSING
|
||||||
|
),
|
||||||
|
"responding": make_entity_target_state_trigger(
|
||||||
|
DOMAIN, AssistSatelliteState.RESPONDING
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||||
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
|
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@MatsNL"],
|
"codeowners": ["@MatsNL"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/atag",
|
"documentation": "https://www.home-assistant.io/integrations/atag",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyatag"],
|
"loggers": ["pyatag"],
|
||||||
"requirements": ["pyatag==0.3.5.3"]
|
"requirements": ["pyatag==0.3.5.3"]
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.1"]
|
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@djtimca"],
|
"codeowners": ["@djtimca"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["auroranoaa"],
|
"loggers": ["auroranoaa"],
|
||||||
"requirements": ["auroranoaa==0.0.5"]
|
"requirements": ["auroranoaa==0.0.5"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@nickw444", "@Bre77"],
|
"codeowners": ["@nickw444", "@Bre77"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aussiebb"],
|
"loggers": ["aussiebb"],
|
||||||
"requirements": ["pyaussiebb==0.1.5"]
|
"requirements": ["pyaussiebb==0.1.5"]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"codeowners": ["@klaasnicolaas"],
|
"codeowners": ["@klaasnicolaas"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
"quality_scale": "silver",
|
||||||
"requirements": ["autarco==3.2.0"]
|
"requirements": ["autarco==3.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ rules:
|
|||||||
This integration does not provide additional actions.
|
This integration does not provide additional actions.
|
||||||
appropriate-polling: done
|
appropriate-polling: done
|
||||||
brands: done
|
brands: done
|
||||||
common-modules:
|
common-modules: done
|
||||||
status: todo
|
|
||||||
comment: |
|
|
||||||
The entity.py file is not used in this integration.
|
|
||||||
config-flow-test-coverage: done
|
config-flow-test-coverage: done
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
|
|||||||
@@ -204,13 +204,25 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoBatterySensorEntity(
|
class AutarcoSensorBase(CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity):
|
||||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
"""Base class for Autarco sensors."""
|
||||||
):
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AutarcoDataUpdateCoordinator,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Autarco sensor base."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
|
||||||
|
class AutarcoBatterySensorEntity(AutarcoSensorBase):
|
||||||
"""Defines an Autarco battery sensor."""
|
"""Defines an Autarco battery sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoBatterySensorEntityDescription
|
entity_description: AutarcoBatterySensorEntityDescription
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -218,10 +230,8 @@ class AutarcoBatterySensorEntity(
|
|||||||
coordinator: AutarcoDataUpdateCoordinator,
|
coordinator: AutarcoDataUpdateCoordinator,
|
||||||
description: AutarcoBatterySensorEntityDescription,
|
description: AutarcoBatterySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco sensor."""
|
"""Initialize Autarco battery sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, description)
|
||||||
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.account_site.site_id}_battery_{description.key}"
|
f"{coordinator.account_site.site_id}_battery_{description.key}"
|
||||||
)
|
)
|
||||||
@@ -239,13 +249,10 @@ class AutarcoBatterySensorEntity(
|
|||||||
return self.entity_description.value_fn(self.coordinator.data.battery)
|
return self.entity_description.value_fn(self.coordinator.data.battery)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoSolarSensorEntity(
|
class AutarcoSolarSensorEntity(AutarcoSensorBase):
|
||||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
|
||||||
):
|
|
||||||
"""Defines an Autarco solar sensor."""
|
"""Defines an Autarco solar sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoSolarSensorEntityDescription
|
entity_description: AutarcoSolarSensorEntityDescription
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -253,10 +260,8 @@ class AutarcoSolarSensorEntity(
|
|||||||
coordinator: AutarcoDataUpdateCoordinator,
|
coordinator: AutarcoDataUpdateCoordinator,
|
||||||
description: AutarcoSolarSensorEntityDescription,
|
description: AutarcoSolarSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco sensor."""
|
"""Initialize Autarco solar sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, description)
|
||||||
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.account_site.site_id}_solar_{description.key}"
|
f"{coordinator.account_site.site_id}_solar_{description.key}"
|
||||||
)
|
)
|
||||||
@@ -273,13 +278,10 @@ class AutarcoSolarSensorEntity(
|
|||||||
return self.entity_description.value_fn(self.coordinator.data.solar)
|
return self.entity_description.value_fn(self.coordinator.data.solar)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoInverterSensorEntity(
|
class AutarcoInverterSensorEntity(AutarcoSensorBase):
|
||||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
|
||||||
):
|
|
||||||
"""Defines an Autarco inverter sensor."""
|
"""Defines an Autarco inverter sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoInverterSensorEntityDescription
|
entity_description: AutarcoInverterSensorEntityDescription
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -288,10 +290,8 @@ class AutarcoInverterSensorEntity(
|
|||||||
description: AutarcoInverterSensorEntityDescription,
|
description: AutarcoInverterSensorEntityDescription,
|
||||||
serial_number: str,
|
serial_number: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco sensor."""
|
"""Initialize Autarco inverter sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, description)
|
||||||
|
|
||||||
self.entity_description = description
|
|
||||||
self._serial_number = serial_number
|
self._serial_number = serial_number
|
||||||
self._attr_unique_id = f"{serial_number}_{description.key}"
|
self._attr_unique_id = f"{serial_number}_{description.key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
|||||||
@@ -124,13 +124,19 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
|||||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
|
"binary_sensor",
|
||||||
|
"button",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
|
"device_tracker",
|
||||||
"fan",
|
"fan",
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"media_player",
|
"media_player",
|
||||||
|
"schedule",
|
||||||
|
"switch",
|
||||||
"text",
|
"text",
|
||||||
|
"update",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"preview_features": {
|
"preview_features": {
|
||||||
"new_triggers_conditions": {
|
"new_triggers_conditions": {
|
||||||
|
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
|
||||||
|
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
|
||||||
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
|
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,10 +69,10 @@
|
|||||||
},
|
},
|
||||||
"preview_features": {
|
"preview_features": {
|
||||||
"new_triggers_conditions": {
|
"new_triggers_conditions": {
|
||||||
"description": "Enables new intuitive triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own intuitive triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
|
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
|
||||||
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new intuitive triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
|
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
|
||||||
"enable_confirmation": "This feature is still in development and may change. These new intuitive triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
|
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
|
||||||
"name": "Intuitive triggers and conditions"
|
"name": "Purpose-specific triggers and conditions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@kaareseras"],
|
"codeowners": ["@kaareseras"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["azure"],
|
"loggers": ["azure"],
|
||||||
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@timmo001"],
|
"codeowners": ["@timmo001"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioazuredevops"],
|
"loggers": ["aioazuredevops"],
|
||||||
"requirements": ["aioazuredevops==2.2.2"]
|
"requirements": ["aioazuredevops==2.2.2"]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@eavanvalkenburg"],
|
"codeowners": ["@eavanvalkenburg"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
|
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["azure"],
|
"loggers": ["azure"],
|
||||||
"requirements": ["azure-eventhub==5.11.1"],
|
"requirements": ["azure-eventhub==5.11.1"],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"codeowners": ["@bdraco", "@jfroy"],
|
"codeowners": ["@bdraco", "@jfroy"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["aiobafi6==0.9.0"],
|
"requirements": ["aiobafi6==0.9.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||||
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pybalboa"],
|
"loggers": ["pybalboa"],
|
||||||
"requirements": ["pybalboa==1.1.3"]
|
"requirements": ["pybalboa==1.1.3"]
|
||||||
|
|||||||
@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
|
|||||||
from homeassistant.util.ssl import get_default_context
|
from homeassistant.util.ssl import get_default_context
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .websocket import BangOlufsenWebsocket
|
from .websocket import BeoWebsocket
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BangOlufsenData:
|
class BeoData:
|
||||||
"""Dataclass for API client and WebSocket client."""
|
"""Dataclass for API client and WebSocket client."""
|
||||||
|
|
||||||
websocket: BangOlufsenWebsocket
|
websocket: BeoWebsocket
|
||||||
client: MozartClient
|
client: MozartClient
|
||||||
|
|
||||||
|
|
||||||
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
|
type BeoConfigEntry = ConfigEntry[BeoData]
|
||||||
|
|
||||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||||
"""Set up from a config entry."""
|
"""Set up from a config entry."""
|
||||||
|
|
||||||
# Remove casts to str
|
# Remove casts to str
|
||||||
assert entry.unique_id
|
assert entry.unique_id
|
||||||
|
|
||||||
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
|
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
|
|||||||
await client.close_api_client()
|
await client.close_api_client()
|
||||||
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
|
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
|
||||||
|
|
||||||
websocket = BangOlufsenWebsocket(hass, entry, client)
|
websocket = BeoWebsocket(hass, entry, client)
|
||||||
|
|
||||||
# Add the websocket and API client
|
# Add the websocket and API client
|
||||||
entry.runtime_data = BangOlufsenData(websocket, client)
|
entry.runtime_data = BeoData(websocket, client)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@@ -82,9 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: BangOlufsenConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
# Close the API client and WebSocket notification listener
|
# Close the API client and WebSocket notification listener
|
||||||
entry.runtime_data.client.disconnect_notifications()
|
entry.runtime_data.client.disconnect_notifications()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ _exception_map = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow."""
|
"""Handle a config flow."""
|
||||||
|
|
||||||
_beolink_jid = ""
|
_beolink_jid = ""
|
||||||
|
|||||||
@@ -14,21 +14,26 @@ from homeassistant.components.media_player import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenSource:
|
class BeoSource:
|
||||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||||
|
|
||||||
|
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
||||||
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||||
|
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
||||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||||
|
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
||||||
|
TV: Final[Source] = Source(name="TV", id="tv")
|
||||||
|
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
|
||||||
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||||
|
|
||||||
|
|
||||||
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
BEO_STATES: dict[str, MediaPlayerState] = {
|
||||||
# Dict used for translating device states to Home Assistant states.
|
# Dict used for translating device states to Home Assistant states.
|
||||||
"started": MediaPlayerState.PLAYING,
|
"started": MediaPlayerState.PLAYING,
|
||||||
"buffering": MediaPlayerState.PLAYING,
|
"buffering": MediaPlayerState.PLAYING,
|
||||||
"idle": MediaPlayerState.IDLE,
|
"idle": MediaPlayerState.IDLE,
|
||||||
"paused": MediaPlayerState.PAUSED,
|
"paused": MediaPlayerState.PAUSED,
|
||||||
"stopped": MediaPlayerState.PAUSED,
|
"stopped": MediaPlayerState.IDLE,
|
||||||
"ended": MediaPlayerState.PAUSED,
|
"ended": MediaPlayerState.PAUSED,
|
||||||
"error": MediaPlayerState.IDLE,
|
"error": MediaPlayerState.IDLE,
|
||||||
# A device's initial state is "unknown" and should be treated as "idle"
|
# A device's initial state is "unknown" and should be treated as "idle"
|
||||||
@@ -36,30 +41,31 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Dict used for translating Home Assistant settings to device repeat settings.
|
# Dict used for translating Home Assistant settings to device repeat settings.
|
||||||
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
||||||
RepeatMode.ALL: "all",
|
RepeatMode.ALL: "all",
|
||||||
RepeatMode.ONE: "track",
|
RepeatMode.ONE: "track",
|
||||||
RepeatMode.OFF: "none",
|
RepeatMode.OFF: "none",
|
||||||
}
|
}
|
||||||
# Dict used for translating device repeat settings to Home Assistant settings.
|
# Dict used for translating device repeat settings to Home Assistant settings.
|
||||||
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
||||||
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
|
value: key for key, value in BEO_REPEAT_FROM_HA.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Media types for play_media
|
# Media types for play_media
|
||||||
class BangOlufsenMediaType(StrEnum):
|
class BeoMediaType(StrEnum):
|
||||||
"""Bang & Olufsen specific media types."""
|
"""Bang & Olufsen specific media types."""
|
||||||
|
|
||||||
FAVOURITE = "favourite"
|
|
||||||
DEEZER = "deezer"
|
DEEZER = "deezer"
|
||||||
|
FAVOURITE = "favourite"
|
||||||
|
OVERLAY_TTS = "overlay_tts"
|
||||||
RADIO = "radio"
|
RADIO = "radio"
|
||||||
TIDAL = "tidal"
|
TIDAL = "tidal"
|
||||||
TTS = "provider"
|
TTS = "provider"
|
||||||
OVERLAY_TTS = "overlay_tts"
|
TV = "tv"
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenModel(StrEnum):
|
class BeoModel(StrEnum):
|
||||||
"""Enum for compatible model names."""
|
"""Enum for compatible model names."""
|
||||||
|
|
||||||
# Mozart devices
|
# Mozart devices
|
||||||
@@ -78,8 +84,18 @@ class BangOlufsenModel(StrEnum):
|
|||||||
BEOREMOTE_ONE = "Beoremote One"
|
BEOREMOTE_ONE = "Beoremote One"
|
||||||
|
|
||||||
|
|
||||||
|
class BeoAttribute(StrEnum):
|
||||||
|
"""Enum for extra_state_attribute keys."""
|
||||||
|
|
||||||
|
BEOLINK = "beolink"
|
||||||
|
BEOLINK_PEERS = "peers"
|
||||||
|
BEOLINK_SELF = "self"
|
||||||
|
BEOLINK_LEADER = "leader"
|
||||||
|
BEOLINK_LISTENERS = "listeners"
|
||||||
|
|
||||||
|
|
||||||
# Physical "buttons" on devices
|
# Physical "buttons" on devices
|
||||||
class BangOlufsenButtons(StrEnum):
|
class BeoButtons(StrEnum):
|
||||||
"""Enum for device buttons."""
|
"""Enum for device buttons."""
|
||||||
|
|
||||||
BLUETOOTH = "Bluetooth"
|
BLUETOOTH = "Bluetooth"
|
||||||
@@ -126,7 +142,7 @@ class WebsocketNotification(StrEnum):
|
|||||||
DOMAIN: Final[str] = "bang_olufsen"
|
DOMAIN: Final[str] = "bang_olufsen"
|
||||||
|
|
||||||
# Default values for configuration.
|
# Default values for configuration.
|
||||||
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
|
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
|
||||||
|
|
||||||
# Configuration.
|
# Configuration.
|
||||||
CONF_SERIAL_NUMBER: Final = "serial_number"
|
CONF_SERIAL_NUMBER: Final = "serial_number"
|
||||||
@@ -134,7 +150,7 @@ CONF_BEOLINK_JID: Final = "jid"
|
|||||||
|
|
||||||
# Models to choose from in manual configuration.
|
# Models to choose from in manual configuration.
|
||||||
SELECTABLE_MODELS: list[str] = [
|
SELECTABLE_MODELS: list[str] = [
|
||||||
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
|
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
|
||||||
]
|
]
|
||||||
|
|
||||||
MANUFACTURER: Final[str] = "Bang & Olufsen"
|
MANUFACTURER: Final[str] = "Bang & Olufsen"
|
||||||
@@ -146,15 +162,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
|
|||||||
ATTR_FRIENDLY_NAME: Final[str] = "fn"
|
ATTR_FRIENDLY_NAME: Final[str] = "fn"
|
||||||
|
|
||||||
# Power states.
|
# Power states.
|
||||||
BANG_OLUFSEN_ON: Final[str] = "on"
|
BEO_ON: Final[str] = "on"
|
||||||
|
|
||||||
VALID_MEDIA_TYPES: Final[tuple] = (
|
VALID_MEDIA_TYPES: Final[tuple] = (
|
||||||
BangOlufsenMediaType.FAVOURITE,
|
BeoMediaType.FAVOURITE,
|
||||||
BangOlufsenMediaType.DEEZER,
|
BeoMediaType.DEEZER,
|
||||||
BangOlufsenMediaType.RADIO,
|
BeoMediaType.RADIO,
|
||||||
BangOlufsenMediaType.TTS,
|
BeoMediaType.TTS,
|
||||||
BangOlufsenMediaType.TIDAL,
|
BeoMediaType.TIDAL,
|
||||||
BangOlufsenMediaType.OVERLAY_TTS,
|
BeoMediaType.OVERLAY_TTS,
|
||||||
MediaType.MUSIC,
|
MediaType.MUSIC,
|
||||||
MediaType.URL,
|
MediaType.URL,
|
||||||
MediaType.CHANNEL,
|
MediaType.CHANNEL,
|
||||||
@@ -232,7 +248,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Device events
|
# Device events
|
||||||
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
|
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
|
||||||
|
|
||||||
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
|
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
|
||||||
EVENT_TRANSLATION_MAP: dict[str, str] = {
|
EVENT_TRANSLATION_MAP: dict[str, str] = {
|
||||||
@@ -249,7 +265,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
|
|||||||
|
|
||||||
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
||||||
|
|
||||||
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
|
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
|
||||||
|
|
||||||
|
|
||||||
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
|
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import BangOlufsenConfigEntry
|
from . import BeoConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .util import get_device_buttons
|
from .util import get_device_buttons
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
|
hass: HomeAssistant, config_entry: BeoConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenBase:
|
class BeoBase:
|
||||||
"""Base class for BangOlufsen Home Assistant objects."""
|
"""Base class for Bang & Olufsen Home Assistant objects."""
|
||||||
|
|
||||||
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
|
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
|
||||||
"""Initialize the object."""
|
"""Initialize the object."""
|
||||||
@@ -51,8 +51,8 @@ class BangOlufsenBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenEntity(Entity, BangOlufsenBase):
|
class BeoEntity(Entity, BeoBase):
|
||||||
"""Base Entity for BangOlufsen entities."""
|
"""Base Entity for Bang & Olufsen entities."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import BangOlufsenConfigEntry
|
from . import BeoConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
BEO_REMOTE_CONTROL_KEYS,
|
BEO_REMOTE_CONTROL_KEYS,
|
||||||
BEO_REMOTE_KEY_EVENTS,
|
BEO_REMOTE_KEY_EVENTS,
|
||||||
@@ -25,10 +25,10 @@ from .const import (
|
|||||||
DEVICE_BUTTON_EVENTS,
|
DEVICE_BUTTON_EVENTS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
BangOlufsenModel,
|
BeoModel,
|
||||||
WebsocketNotification,
|
WebsocketNotification,
|
||||||
)
|
)
|
||||||
from .entity import BangOlufsenEntity
|
from .entity import BeoEntity
|
||||||
from .util import get_device_buttons, get_remotes
|
from .util import get_device_buttons, get_remotes
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: BangOlufsenConfigEntry,
|
config_entry: BeoConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Event entities from config entry."""
|
"""Set up Event entities from config entry."""
|
||||||
entities: list[BangOlufsenEvent] = []
|
entities: list[BeoEvent] = []
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
BangOlufsenButtonEvent(config_entry, button_type)
|
BeoButtonEvent(config_entry, button_type)
|
||||||
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ async def async_setup_entry(
|
|||||||
# Add Light keys
|
# Add Light keys
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
BangOlufsenRemoteKeyEvent(
|
BeoRemoteKeyEvent(
|
||||||
config_entry,
|
config_entry,
|
||||||
remote,
|
remote,
|
||||||
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
|
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
|
||||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
|||||||
# Add Control keys
|
# Add Control keys
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
BangOlufsenRemoteKeyEvent(
|
BeoRemoteKeyEvent(
|
||||||
config_entry,
|
config_entry,
|
||||||
remote,
|
remote,
|
||||||
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
|
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
|
||||||
@@ -84,10 +84,9 @@ async def async_setup_entry(
|
|||||||
config_entry.entry_id
|
config_entry.entry_id
|
||||||
)
|
)
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if (
|
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
|
||||||
device.model == BangOlufsenModel.BEOREMOTE_ONE
|
remote.serial_number for remote in remotes
|
||||||
and device.serial_number not in {remote.serial_number for remote in remotes}
|
}:
|
||||||
):
|
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id, remove_config_entry_id=config_entry.entry_id
|
device.id, remove_config_entry_id=config_entry.entry_id
|
||||||
)
|
)
|
||||||
@@ -95,13 +94,13 @@ async def async_setup_entry(
|
|||||||
async_add_entities(new_entities=entities)
|
async_add_entities(new_entities=entities)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
|
class BeoEvent(BeoEntity, EventEntity):
|
||||||
"""Base Event class."""
|
"""Base Event class."""
|
||||||
|
|
||||||
_attr_device_class = EventDeviceClass.BUTTON
|
_attr_device_class = EventDeviceClass.BUTTON
|
||||||
_attr_entity_registry_enabled_default = False
|
_attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
|
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
||||||
"""Initialize Event."""
|
"""Initialize Event."""
|
||||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||||
|
|
||||||
@@ -112,12 +111,12 @@ class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenButtonEvent(BangOlufsenEvent):
|
class BeoButtonEvent(BeoEvent):
|
||||||
"""Event class for Button events."""
|
"""Event class for Button events."""
|
||||||
|
|
||||||
_attr_event_types = DEVICE_BUTTON_EVENTS
|
_attr_event_types = DEVICE_BUTTON_EVENTS
|
||||||
|
|
||||||
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
|
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
|
||||||
"""Initialize Button."""
|
"""Initialize Button."""
|
||||||
super().__init__(config_entry)
|
super().__init__(config_entry)
|
||||||
|
|
||||||
@@ -146,14 +145,14 @@ class BangOlufsenButtonEvent(BangOlufsenEvent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
|
class BeoRemoteKeyEvent(BeoEvent):
|
||||||
"""Event class for Beoremote One key events."""
|
"""Event class for Beoremote One key events."""
|
||||||
|
|
||||||
_attr_event_types = BEO_REMOTE_KEY_EVENTS
|
_attr_event_types = BEO_REMOTE_KEY_EVENTS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config_entry: BangOlufsenConfigEntry,
|
config_entry: BeoConfigEntry,
|
||||||
remote: PairedRemote,
|
remote: PairedRemote,
|
||||||
key_type: str,
|
key_type: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -166,8 +165,8 @@ class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
|
|||||||
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
|
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
|
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
|
||||||
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
|
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
|
||||||
model=BangOlufsenModel.BEOREMOTE_ONE,
|
model=BeoModel.BEOREMOTE_ONE,
|
||||||
serial_number=remote.serial_number,
|
serial_number=remote.serial_number,
|
||||||
sw_version=remote.app_version,
|
sw_version=remote.app_version,
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user