mirror of
https://github.com/home-assistant/core.git
synced 2025-12-19 22:38:02 +00:00
Compare commits
1566 Commits
add-includ
...
scene_trig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4251ea0587 | ||
|
|
065b0eb5b2 | ||
|
|
6a1d86d5db | ||
|
|
f99a73ef28 | ||
|
|
e9305c3846 | ||
|
|
0436d30062 | ||
|
|
24b6b5452b | ||
|
|
8b91ebfe30 | ||
|
|
37d3b73c1b | ||
|
|
c881d9809e | ||
|
|
85dfe3a107 | ||
|
|
d8a468833e | ||
|
|
5bbd56b8e6 | ||
|
|
d0411b6613 | ||
|
|
293fbebef2 | ||
|
|
cfe542acb9 | ||
|
|
8da323d4b7 | ||
|
|
b2edf637cc | ||
|
|
de61a45de1 | ||
|
|
d9324cb0e4 | ||
|
|
4a464f601c | ||
|
|
43e9c24c18 | ||
|
|
1c3492b4c2 | ||
|
|
e0cb56a38c | ||
|
|
6e05cc4898 | ||
|
|
6f9dc2e5a2 | ||
|
|
ddb1ae371d | ||
|
|
6553337b79 | ||
|
|
aedc729d57 | ||
|
|
31fa69b609 | ||
|
|
b819a866b9 | ||
|
|
6cc7d83def | ||
|
|
5154418051 | ||
|
|
7e63c12b95 | ||
|
|
d17e951591 | ||
|
|
9198e5f56d | ||
|
|
97d7e0e01e | ||
|
|
4d5b8c4b08 | ||
|
|
abb011311e | ||
|
|
92cf7623fa | ||
|
|
aedf4c881b | ||
|
|
74baf44c83 | ||
|
|
9afb4a9eb8 | ||
|
|
e1967bef9a | ||
|
|
f17b6aa9e4 | ||
|
|
dd6d7397d9 | ||
|
|
aeabd2d2cc | ||
|
|
d7af2f39c2 | ||
|
|
a674ad11bc | ||
|
|
ccb64d7fd8 | ||
|
|
36691e2a3d | ||
|
|
8971f75f13 | ||
|
|
173db170af | ||
|
|
881851a4f6 | ||
|
|
4b4b64e939 | ||
|
|
e721c1a092 | ||
|
|
0933c9fe51 | ||
|
|
632b3e5dc3 | ||
|
|
434cb48344 | ||
|
|
86d4c3cbbf | ||
|
|
3019f9041c | ||
|
|
9e0a3dee08 | ||
|
|
fefe7d9e5d | ||
|
|
4c382cedff | ||
|
|
6ffd05313b | ||
|
|
9be0214021 | ||
|
|
54300430b7 | ||
|
|
a25038259e | ||
|
|
81be14c8f1 | ||
|
|
62464b83dc | ||
|
|
beb909528c | ||
|
|
ef28715360 | ||
|
|
78cc41fdc0 | ||
|
|
6a868ca5cc | ||
|
|
f43dead38c | ||
|
|
86163252e1 | ||
|
|
0cd5202596 | ||
|
|
33dcde7de1 | ||
|
|
c449b2e2e8 | ||
|
|
f40f7072c8 | ||
|
|
4163ecd833 | ||
|
|
9c59d528af | ||
|
|
c2440c4ebd | ||
|
|
cb275f65ba | ||
|
|
b1923df3ca | ||
|
|
7ddfd155ca | ||
|
|
e01df6d10d | ||
|
|
54010728d5 | ||
|
|
62a3b3827f | ||
|
|
b9abfba20f | ||
|
|
eca9f36e55 | ||
|
|
3c865c6f41 | ||
|
|
3b32c4bcbf | ||
|
|
fcdc1cfed9 | ||
|
|
0fd782c4ab | ||
|
|
bbcaf69973 | ||
|
|
f2b713acac | ||
|
|
6c944d6b15 | ||
|
|
4dd3abb16a | ||
|
|
d2672b9ddf | ||
|
|
ff30492919 | ||
|
|
b5ccdf8165 | ||
|
|
b3c745cfa7 | ||
|
|
67aeafa797 | ||
|
|
3d71b6de44 | ||
|
|
5349045932 | ||
|
|
4960871c84 | ||
|
|
af3861cd6b | ||
|
|
f9a070e9b3 | ||
|
|
fd503b2e33 | ||
|
|
e5a73fcf57 | ||
|
|
6991e01489 | ||
|
|
c8636ee6f3 | ||
|
|
52229dc5a8 | ||
|
|
f013455843 | ||
|
|
cae5bca546 | ||
|
|
49299b06c6 | ||
|
|
8e39027ad5 | ||
|
|
2a1ce2df61 | ||
|
|
7a6d929150 | ||
|
|
6f4a112dbb | ||
|
|
2197b910fb | ||
|
|
7e2a9cd7f9 | ||
|
|
e7ed7a8ed2 | ||
|
|
9ba2d0defe | ||
|
|
231300919c | ||
|
|
664c50586f | ||
|
|
43b9ecfc2b | ||
|
|
f1237ed52a | ||
|
|
ecf8f55cc4 | ||
|
|
ff36693057 | ||
|
|
005785997c | ||
|
|
9917b82b66 | ||
|
|
9c927406ac | ||
|
|
972d95602a | ||
|
|
5e0549a18f | ||
|
|
bcbb159fb2 | ||
|
|
0123ca656a | ||
|
|
1f699c729c | ||
|
|
50c3fcfeba | ||
|
|
2af1e098cc | ||
|
|
c418d9750b | ||
|
|
e96d614076 | ||
|
|
f0a5e0a023 | ||
|
|
6ac6b86060 | ||
|
|
3909171b1a | ||
|
|
769029505f | ||
|
|
080ec3524b | ||
|
|
48d671ad5f | ||
|
|
7115db5d22 | ||
|
|
d0c8792e4b | ||
|
|
84d7c37502 | ||
|
|
937ccc7867 | ||
|
|
385ab8f5d3 | ||
|
|
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 | ||
|
|
c82706eaf5 | ||
|
|
07f9bec8b6 | ||
|
|
33d576234b | ||
|
|
9e2b4615f1 | ||
|
|
a46dc7e05f | ||
|
|
7dd9953345 | ||
|
|
1145026190 | ||
|
|
d8f9574bc3 | ||
|
|
e91f8d3a81 | ||
|
|
8c0fd0565e | ||
|
|
cc620fc0f8 | ||
|
|
5a89332680 | ||
|
|
1831c5e249 | ||
|
|
dddd2503ea | ||
|
|
91ba510a1e | ||
|
|
6e5e739496 | ||
|
|
6b39eb069c | ||
|
|
847c332c70 | ||
|
|
1a19f3b527 | ||
|
|
8110935d2d | ||
|
|
af69da94f5 | ||
|
|
c1cf17d4db | ||
|
|
6079637909 | ||
|
|
9268e12b20 | ||
|
|
d07993f4a4 | ||
|
|
441cb4197c | ||
|
|
d2a095588d | ||
|
|
f2578da7db | ||
|
|
22200d6804 | ||
|
|
8a4e5c3a28 | ||
|
|
30f31c7d8c | ||
|
|
232c4255a1 | ||
|
|
236f7cd22c | ||
|
|
5948ff2e31 | ||
|
|
380127bc70 | ||
|
|
b6a1e8251a | ||
|
|
c20236717c | ||
|
|
1fd9feaace | ||
|
|
7ce072b4dc | ||
|
|
45aa0399c7 | ||
|
|
d82b3871c1 | ||
|
|
8f6d1162e5 | ||
|
|
dafce97341 | ||
|
|
ffd5d33bbc | ||
|
|
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 | ||
|
|
bac32bc379 | ||
|
|
6344837009 | ||
|
|
9079ff5ea8 | ||
|
|
cd646aea11 | ||
|
|
b3a93d9fab | ||
|
|
db98fb138b | ||
|
|
348c8bca7c | ||
|
|
e30707ad5e | ||
|
|
3fa4dcb980 | ||
|
|
57835efc9d | ||
|
|
4646929987 | ||
|
|
010aea952c | ||
|
|
563678dc47 | ||
|
|
a48f01f213 | ||
|
|
08b758b0d2 | ||
|
|
4306fbea52 | ||
|
|
6f4c479f8f | ||
|
|
1d9c06264e | ||
|
|
d045ecaf13 | ||
|
|
f7c41e694c | ||
|
|
9ee7ed5cdb | ||
|
|
83c4e2abc9 | ||
|
|
f8d5a8bc58 | ||
|
|
3f1f8da6f5 | ||
|
|
55613f56b6 | ||
|
|
3ee2a78663 | ||
|
|
814a0c4cc9 | ||
|
|
71b674d8f1 | ||
|
|
c952fc5e31 | ||
|
|
8c3d40a348 | ||
|
|
2451dfb63d | ||
|
|
8e5921eab6 | ||
|
|
bc730da9b1 | ||
|
|
28b7ebea6e | ||
|
|
cfa447c7a9 | ||
|
|
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 | ||
|
|
f64c870e42 | ||
|
|
4654d6de87 | ||
|
|
990c8cd4e6 | ||
|
|
f8c76f42e3 | ||
|
|
21d914c8ca | ||
|
|
ec77add1a6 | ||
|
|
ef3b7dfd1d | ||
|
|
51241d963d | ||
|
|
7c48e6e046 | ||
|
|
38d8da4279 | ||
|
|
3396a72fa8 | ||
|
|
2d26ab390e | ||
|
|
1bf5bc9323 | ||
|
|
87ea96a3e0 | ||
|
|
e3cf65510b | ||
|
|
f69fce68d6 | ||
|
|
f758cfa82f | ||
|
|
9c7a928b29 | ||
|
|
405a9948a2 | ||
|
|
0e3bab3ce4 | ||
|
|
4900d25ac8 | ||
|
|
ea10cdb4b0 | ||
|
|
6baf77d256 | ||
|
|
13bc0ebed8 | ||
|
|
611af9c832 | ||
|
|
c2b7a63dd9 | ||
|
|
550716a753 | ||
|
|
56a71e6798 | ||
|
|
80ec51c56b | ||
|
|
ea651c4a22 | ||
|
|
ff40ce419e | ||
|
|
d95308719c | ||
|
|
f4fb95ee43 | ||
|
|
14d95cc86b | ||
|
|
4257435975 | ||
|
|
a6aab088fb | ||
|
|
655a63c104 | ||
|
|
a2ade413c2 | ||
|
|
10299b2ef4 | ||
|
|
26444d8d34 | ||
|
|
554c122a37 | ||
|
|
1c0dd02a7c | ||
|
|
c41493860d | ||
|
|
f5b8ede5f9 | ||
|
|
474a60511b | ||
|
|
9657f3f832 | ||
|
|
59f4bc1908 | ||
|
|
9ebc6cbb23 | ||
|
|
f433ca7455 | ||
|
|
f77629d0f8 | ||
|
|
0ac1b22e03 | ||
|
|
1069233851 | ||
|
|
d2fd200469 | ||
|
|
20cdd9386e | ||
|
|
1be2e4f90c | ||
|
|
839f647396 | ||
|
|
7c2741bd36 | ||
|
|
d6fb268119 | ||
|
|
521a6784b4 | ||
|
|
d2ba7e8e3e | ||
|
|
405c2f96fd | ||
|
|
90ef5b1c25 | ||
|
|
562f72f321 | ||
|
|
f5ee3bd872 | ||
|
|
8dd35cb129 | ||
|
|
6fa971d393 | ||
|
|
6deff1c78f | ||
|
|
f96996b27f | ||
|
|
eb9fc66ca9 | ||
|
|
43e4fe4526 | ||
|
|
252dbb706f | ||
|
|
d7ad0cba94 | ||
|
|
159a8d39d6 | ||
|
|
8f1abb6dbb | ||
|
|
242c02890f | ||
|
|
eb793a3942 | ||
|
|
e65c47ba0f | ||
|
|
24dba24571 | ||
|
|
4c04dc00dd | ||
|
|
0c366506c5 | ||
|
|
a0323e80f5 | ||
|
|
e496fb2227 | ||
|
|
c2219aadb1 | ||
|
|
2cd0637324 | ||
|
|
293f8f7c87 | ||
|
|
1af569ae17 | ||
|
|
d4db5ec0cc | ||
|
|
4be1fa9a3a | ||
|
|
149c1e6772 | ||
|
|
e37e7574a4 | ||
|
|
37152a27ba | ||
|
|
5025af8334 | ||
|
|
ec9fb9837a | ||
|
|
30451e3aaa | ||
|
|
c66c3497c1 | ||
|
|
1063e71318 | ||
|
|
1a875b021a | ||
|
|
15328a4aff | ||
|
|
083cfb89af | ||
|
|
bd129c2085 | ||
|
|
f73bc9242b | ||
|
|
4506be5065 | ||
|
|
80c611e562 | ||
|
|
b44aafc294 | ||
|
|
af1e3205b8 | ||
|
|
1360fe7f23 | ||
|
|
b5bb8583f8 | ||
|
|
9b62c212ce | ||
|
|
8fa56ad92e | ||
|
|
f82f0a1862 | ||
|
|
878881b100 | ||
|
|
743583d9bd | ||
|
|
f537204d22 | ||
|
|
ec74be7922 | ||
|
|
3574f647d0 | ||
|
|
6c4296a0de | ||
|
|
e780e3db8c | ||
|
|
4ed2efa4e8 | ||
|
|
abef6f7b3e | ||
|
|
5556fb99e6 | ||
|
|
16669e39bd | ||
|
|
ca088d81c3 | ||
|
|
12847fb0a4 | ||
|
|
8b758c46f4 | ||
|
|
f439471dc1 | ||
|
|
5ff3233b09 | ||
|
|
22daed083f | ||
|
|
13384de464 | ||
|
|
f5e5183190 | ||
|
|
e18668b8f9 | ||
|
|
15647f2720 | ||
|
|
c961126ee5 | ||
|
|
5142c5f418 | ||
|
|
3d459704e1 | ||
|
|
5a8ddcd0b3 | ||
|
|
2667a40b92 | ||
|
|
08baa99691 | ||
|
|
d84cf26f40 | ||
|
|
ba5472da90 | ||
|
|
e20b88a54f | ||
|
|
ac69712a51 | ||
|
|
f0e75ba0ed | ||
|
|
e64598e7f5 | ||
|
|
e6f9a8e7d6 | ||
|
|
1e8b42f843 | ||
|
|
430eee0b28 | ||
|
|
b4799aa7ea | ||
|
|
ab45460069 | ||
|
|
c8fd6db3ff | ||
|
|
0a9f200ca4 | ||
|
|
8591335660 | ||
|
|
c01089e994 | ||
|
|
79a7daf89d | ||
|
|
d22867b852 | ||
|
|
ddb74c5af4 | ||
|
|
9aec7b12c2 | ||
|
|
bf42e3769a | ||
|
|
43f40c6f0e | ||
|
|
03ac634e6d | ||
|
|
a204e85d84 | ||
|
|
79c7ad7646 | ||
|
|
704d4c896d | ||
|
|
5b6a4b0fea | ||
|
|
ef5573c693 | ||
|
|
45aecd525a | ||
|
|
ce1146492e | ||
|
|
1ce890b105 | ||
|
|
3e7bef77e5 | ||
|
|
1222828852 | ||
|
|
1ef64582eb | ||
|
|
d363bd63eb | ||
|
|
5916af1115 | ||
|
|
f8bf7ec1ff | ||
|
|
41e42b9581 | ||
|
|
51f68f2776 | ||
|
|
773cb7424c | ||
|
|
eefab75ef0 | ||
|
|
81b4122b73 | ||
|
|
bd0ab4d1fe | ||
|
|
80151b205d | ||
|
|
4488fdd2d6 | ||
|
|
a6e0bea805 | ||
|
|
994619e179 | ||
|
|
4db5be73a7 | ||
|
|
3cfedd1721 | ||
|
|
2f1301abaf | ||
|
|
21d61ef401 | ||
|
|
6850f9622a | ||
|
|
2b2bb79505 | ||
|
|
d97998e2e1 | ||
|
|
3ef62c97ca | ||
|
|
5cca95ab2f | ||
|
|
a4f0a21c8e | ||
|
|
11a2b5df6a | ||
|
|
07e2c8a610 | ||
|
|
43783ed896 | ||
|
|
a206604df5 | ||
|
|
2e82ac81b2 | ||
|
|
5139e9e566 | ||
|
|
c53674531c | ||
|
|
a04244ad25 | ||
|
|
b27b357b91 | ||
|
|
01e38853c0 | ||
|
|
06158fc9a1 | ||
|
|
e5968084a2 | ||
|
|
263839a6c0 | ||
|
|
931b2c2db0 | ||
|
|
8e26112db1 | ||
|
|
b1286af423 | ||
|
|
bd02e279cf | ||
|
|
6e5be843d6 | ||
|
|
5b1d86a04b | ||
|
|
1514013c3b | ||
|
|
54ed290cc1 | ||
|
|
1106f4f0e2 | ||
|
|
f73e92a34a | ||
|
|
74ad5066e2 | ||
|
|
4202a665af | ||
|
|
c9ddbe39ce | ||
|
|
8a2e8d2c61 | ||
|
|
ca2e8bfb56 | ||
|
|
c0772f3957 | ||
|
|
0b96da3b24 | ||
|
|
4c07b2b290 | ||
|
|
f699d95ea0 | ||
|
|
f6b9a0eb29 | ||
|
|
71c665ed49 | ||
|
|
85a1afb174 | ||
|
|
9668a68c28 | ||
|
|
a06aa8edfe | ||
|
|
4e30a5d930 | ||
|
|
696550a7f2 | ||
|
|
c064d23a99 | ||
|
|
ac7b063c2c | ||
|
|
e0778c8e2e | ||
|
|
2ba5a96d5b | ||
|
|
13c9fb6e37 | ||
|
|
102bb1f694 | ||
|
|
fc8f8b39b4 | ||
|
|
e5b2d44e8e | ||
|
|
ec0918027e | ||
|
|
8a54f8d4e2 | ||
|
|
5c27126b6d | ||
|
|
e069aff0e2 | ||
|
|
733526fae3 | ||
|
|
1ef001f8e9 | ||
|
|
7732377fde | ||
|
|
b7786e589b | ||
|
|
4f60970a91 | ||
|
|
1c1286dd57 | ||
|
|
41c9f08f60 | ||
|
|
fc4bfab0f7 | ||
|
|
769a12f74e | ||
|
|
dabaa2bc5e | ||
|
|
b674828a91 | ||
|
|
761da66658 | ||
|
|
c8aba62301 | ||
|
|
07ab2e6805 | ||
|
|
f62e0c8c08 | ||
|
|
6ca00f9dbb | ||
|
|
0fba80e30f | ||
|
|
7073c40385 | ||
|
|
8fb9d92daf | ||
|
|
2d81665f99 | ||
|
|
b398935539 | ||
|
|
95f588aae1 | ||
|
|
ffe524d95a | ||
|
|
4d4ad900b1 | ||
|
|
acc136af19 | ||
|
|
0f12a40eb2 | ||
|
|
bf124daf72 | ||
|
|
1682ced5cc | ||
|
|
80b316bc70 | ||
|
|
00d2340d4b | ||
|
|
514a329580 | ||
|
|
f2b8bb01bf | ||
|
|
30153ab059 | ||
|
|
2957b15ede | ||
|
|
12ace95f3e | ||
|
|
babe19767d | ||
|
|
d01843e1ab | ||
|
|
9964cb512a | ||
|
|
ae38214b7c | ||
|
|
9812286801 | ||
|
|
32a40e5919 | ||
|
|
97de944a14 | ||
|
|
c9bd87f4b3 | ||
|
|
ac46568996 | ||
|
|
7c1b8ee02c | ||
|
|
aa6901265d | ||
|
|
b76e9ad1c0 | ||
|
|
edb8007c65 | ||
|
|
956a29411f | ||
|
|
1a2361050b | ||
|
|
0c9e92f6f9 | ||
|
|
bfdff46859 | ||
|
|
9a22808499 | ||
|
|
88b373af41 | ||
|
|
dea2f37e8f | ||
|
|
30cce68e0b | ||
|
|
985eff972a | ||
|
|
31ca332158 | ||
|
|
bf76c1601d | ||
|
|
e572f8d48f | ||
|
|
482b5d49a3 | ||
|
|
126fd217e7 | ||
|
|
0327b0e1ec | ||
|
|
3d5a7b4813 | ||
|
|
e0bb30f63b | ||
|
|
e5ae58c5df | ||
|
|
13e4bb4b93 | ||
|
|
d5fd27d2a2 | ||
|
|
0a034b9984 | ||
|
|
6a8106c0eb | ||
|
|
2cacfc7413 | ||
|
|
388ab5c16c | ||
|
|
81ea6f8c25 | ||
|
|
4f885994b7 | ||
|
|
25e2c9ee80 | ||
|
|
12c04f5571 | ||
|
|
3ad1c6a47a | ||
|
|
e7e13ecc74 | ||
|
|
991b8d2040 | ||
|
|
43fadbf6b4 | ||
|
|
ca79d37135 | ||
|
|
df8ef15535 | ||
|
|
249c1530d0 | ||
|
|
081b769abc | ||
|
|
b8b101d747 | ||
|
|
a19be192e0 | ||
|
|
92da82a200 | ||
|
|
820ba1dfba | ||
|
|
63c8962f09 | ||
|
|
c1a6996549 | ||
|
|
05253841af | ||
|
|
f2ef0503a0 | ||
|
|
938da38fc3 | ||
|
|
9311a87bf5 | ||
|
|
b45294ded3 | ||
|
|
82d3190016 | ||
|
|
d8cbcc1977 | ||
|
|
4b69543515 | ||
|
|
97ef4a35b9 | ||
|
|
f782c78650 | ||
|
|
139ed34c74 | ||
|
|
7f14d013ac | ||
|
|
963e27dda4 | ||
|
|
b8e3d57fea | ||
|
|
0de2a16d0f | ||
|
|
c8c2413a09 | ||
|
|
291331f878 | ||
|
|
a13cdbdf3d | ||
|
|
1bf713f279 | ||
|
|
10c8ee417b | ||
|
|
b23134f4f1 | ||
|
|
f45a6f806b | ||
|
|
d3857a00d5 | ||
|
|
8c9b90a9f9 | ||
|
|
4eedc88935 | ||
|
|
343ea1b82d | ||
|
|
36e13653d2 | ||
|
|
80444b2165 | ||
|
|
262f06dd2b | ||
|
|
bd87119c2e | ||
|
|
0dfa037aa8 | ||
|
|
c32a471573 | ||
|
|
97b7e51171 | ||
|
|
433712b407 | ||
|
|
5d87e0f429 | ||
|
|
acb087f1e5 | ||
|
|
10c12623bf | ||
|
|
2fe20553b3 | ||
|
|
b431bb197a | ||
|
|
eb9d625926 | ||
|
|
3a69534b09 | ||
|
|
8f2cedcb73 | ||
|
|
3658953ff3 | ||
|
|
0be5893e37 | ||
|
|
c87e38c4cf | ||
|
|
4874610ad6 | ||
|
|
9180282fc6 | ||
|
|
118f30f32e | ||
|
|
bd10da126f | ||
|
|
b73a7928ca | ||
|
|
3e20c2ea93 | ||
|
|
60130d3d68 | ||
|
|
c45ede2e5d | ||
|
|
e167061f53 | ||
|
|
5560fb6c9e | ||
|
|
9808b6c961 | ||
|
|
e8cfde579e | ||
|
|
f695fb4d51 | ||
|
|
a0e0549d90 | ||
|
|
ba034c6c8c | ||
|
|
008bb85c59 | ||
|
|
cf1c1294d3 | ||
|
|
11d5d314cc | ||
|
|
6f0de3071a | ||
|
|
87d2597292 | ||
|
|
437bc04fe8 | ||
|
|
67a0d6a187 | ||
|
|
abb52bca81 | ||
|
|
d2d6889278 | ||
|
|
bdca592219 | ||
|
|
5c0c7b9ec3 | ||
|
|
9717599fb9 | ||
|
|
4d7de2f814 | ||
|
|
779590ce1c | ||
|
|
f3a185ff9c | ||
|
|
5a5a106984 | ||
|
|
796b421d99 | ||
|
|
0c03e8dbe9 | ||
|
|
47cf4e3ffe | ||
|
|
0ea0fc151d | ||
|
|
b7e5afec9f | ||
|
|
7a2bb67e82 | ||
|
|
e0612bec07 | ||
|
|
a06f4b6776 | ||
|
|
275670a526 | ||
|
|
d0d62526dd | ||
|
|
aefdf412b0 | ||
|
|
ee05adfca1 | ||
|
|
168c915b5f | ||
|
|
6c80be52af | ||
|
|
ead92cdf82 | ||
|
|
c0f0cfef59 | ||
|
|
cefc0ba96e | ||
|
|
ad091b1062 | ||
|
|
876bc6d8c4 | ||
|
|
9f206d4363 | ||
|
|
a2d11e6d98 | ||
|
|
3b38af3984 | ||
|
|
3875f91bb9 | ||
|
|
c813776b0c | ||
|
|
3afb421cba | ||
|
|
c16633568b | ||
|
|
87f8ff2bb4 | ||
|
|
b423303f1e | ||
|
|
f6ff222679 | ||
|
|
0152fa0c03 | ||
|
|
37ebbe83bc | ||
|
|
63e036d39e | ||
|
|
f0cbf34a78 | ||
|
|
596bc89ee6 | ||
|
|
b8c877e1d2 | ||
|
|
197d9781cb | ||
|
|
f3f323637e | ||
|
|
9748abc103 | ||
|
|
596f049971 | ||
|
|
dee80cb6f5 | ||
|
|
b4ab73468b | ||
|
|
a300199a97 | ||
|
|
09dd765583 | ||
|
|
0c8b765415 | ||
|
|
0824ec502f | ||
|
|
9e0e353a5f | ||
|
|
e934b006e2 | ||
|
|
05479bb8fd | ||
|
|
d07247566d | ||
|
|
19e6097df6 | ||
|
|
2cff3cf29c | ||
|
|
5cac9b8e5e | ||
|
|
c2a516ea32 | ||
|
|
192b38d3e2 | ||
|
|
bb018e3546 | ||
|
|
4919d73cc5 | ||
|
|
56ab6b2512 | ||
|
|
d1dea85cf5 | ||
|
|
84b0d39763 | ||
|
|
3aff225bc3 | ||
|
|
04458e01be | ||
|
|
ae51cfb8c0 | ||
|
|
c116a9c037 | ||
|
|
fb58758684 | ||
|
|
25fbcbc68c | ||
|
|
a670286b45 | ||
|
|
52ba55b17f | ||
|
|
ff0fc98c36 | ||
|
|
9f78a2263d | ||
|
|
9b4696a80b | ||
|
|
70fe8cae39 | ||
|
|
95eb45ab08 | ||
|
|
84f8e57141 | ||
|
|
f484b6df0d | ||
|
|
34c1d45ee0 | ||
|
|
09a105d9ad | ||
|
|
6bd1787d0a | ||
|
|
37040f5064 | ||
|
|
531397ec07 | ||
|
|
d6cc0f81de | ||
|
|
f8ef8a466a | ||
|
|
713015e26a | ||
|
|
f9c1e81c5e | ||
|
|
0549d113e6 | ||
|
|
0d842978ec | ||
|
|
55476ef6ea | ||
|
|
0e130d8fdd | ||
|
|
20bcb84956 | ||
|
|
bbb1d57081 | ||
|
|
121406569b | ||
|
|
4866c775ce | ||
|
|
7c5ab12270 | ||
|
|
099edfac20 | ||
|
|
aa31df0fd5 | ||
|
|
13fbeb6cdb | ||
|
|
8d557447df | ||
|
|
e6e3f2455f | ||
|
|
c9c518ee84 | ||
|
|
214731e964 | ||
|
|
c4b09c9a0a | ||
|
|
f5b5b2fb70 | ||
|
|
bb3cdd382b | ||
|
|
8d09b5c273 | ||
|
|
d92fa7fa72 | ||
|
|
0c45b7f615 | ||
|
|
bfa1116115 | ||
|
|
4984237987 | ||
|
|
3839573151 | ||
|
|
e02dc53df3 | ||
|
|
bedae1e12c | ||
|
|
b4eb73be98 | ||
|
|
0ac3f776fa | ||
|
|
8e8a4fff11 | ||
|
|
579ffcc64d | ||
|
|
81943fb31d | ||
|
|
70dd0bf12e | ||
|
|
c2d462c1e7 | ||
|
|
49e050cc60 | ||
|
|
f6d829a2f3 | ||
|
|
e44e3b6f25 | ||
|
|
af603661c0 | ||
|
|
35c6113777 | ||
|
|
3c2f729ddc | ||
|
|
0d63cb765f | ||
|
|
f3ddffb5ff | ||
|
|
9bdfa77fa0 | ||
|
|
c65003009f | ||
|
|
0f722109b7 | ||
|
|
f7d86dec3c | ||
|
|
6b49c8a70c | ||
|
|
ab9a8f3e53 | ||
|
|
4e12628266 | ||
|
|
e6d8d4de42 | ||
|
|
6620b90eb4 | ||
|
|
6fd3af8891 | ||
|
|
46979b8418 | ||
|
|
1718a11de2 | ||
|
|
2016b1d8c7 | ||
|
|
4b72e45fc2 | ||
|
|
ead5ce905b | ||
|
|
f233f2da3f |
@@ -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",
|
||||||
|
|||||||
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -51,6 +51,9 @@ rules:
|
|||||||
- **Missing imports** - We use static analysis tooling to catch that
|
- **Missing imports** - We use static analysis tooling to catch that
|
||||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
||||||
|
|
||||||
|
**Git commit practices during review:**
|
||||||
|
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||||
|
|
||||||
## Python Requirements
|
## Python Requirements
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
- **Compatibility**: Python 3.13+
|
||||||
|
|||||||
264
.github/workflows/builder.yml
vendored
264
.github/workflows/builder.yml
vendored
@@ -14,6 +14,9 @@ env:
|
|||||||
PIP_TIMEOUT: 60
|
PIP_TIMEOUT: 60
|
||||||
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: "2025.12.0"
|
||||||
|
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
@@ -21,18 +24,16 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
architectures: ${{ steps.info.outputs.architectures }}
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
channel: ${{ steps.version.outputs.channel }}
|
channel: ${{ steps.version.outputs.channel }}
|
||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
|
architectures: ${{ env.ARCHITECTURES }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -69,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
|
||||||
@@ -79,7 +80,7 @@ jobs:
|
|||||||
name: Build ${{ matrix.arch }} base core image
|
name: Build ${{ matrix.arch }} base core image
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -88,13 +89,14 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
exclude:
|
include:
|
||||||
- arch: armv7
|
- arch: amd64
|
||||||
- arch: armhf
|
os: ubuntu-latest
|
||||||
- arch: i386
|
- arch: aarch64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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'
|
||||||
@@ -120,7 +122,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -167,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
|
||||||
|
|
||||||
@@ -188,16 +190,60 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# home-assistant/builder doesn't support sha pinning
|
- &install_cosign
|
||||||
- name: Build base image
|
name: Install Cosign
|
||||||
uses: home-assistant/builder@2025.09.0
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
with:
|
with:
|
||||||
args: |
|
cosign-release: "v2.5.3"
|
||||||
$BUILD_ARGS \
|
|
||||||
--${{ matrix.arch }} \
|
- name: Set up Docker Buildx
|
||||||
--cosign \
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
--target /data \
|
|
||||||
--generic ${{ needs.init.outputs.version }}
|
- name: Build variables
|
||||||
|
id: vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Verify base image signature
|
||||||
|
run: |
|
||||||
|
cosign verify \
|
||||||
|
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||||
|
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
|
||||||
|
"${{ steps.vars.outputs.base_image }}"
|
||||||
|
|
||||||
|
- name: Verify cache image signature
|
||||||
|
id: cache
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
cosign verify \
|
||||||
|
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||||
|
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
|
||||||
|
"${{ steps.vars.outputs.cache_image }}"
|
||||||
|
|
||||||
|
- name: Build base image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: ${{ steps.vars.outputs.platform }}
|
||||||
|
push: true
|
||||||
|
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
||||||
|
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||||
|
labels: |
|
||||||
|
io.hass.arch=${{ matrix.arch }}
|
||||||
|
io.hass.version=${{ needs.init.outputs.version }}
|
||||||
|
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
|
||||||
|
org.opencontainers.image.version=${{ needs.init.outputs.version }}
|
||||||
|
|
||||||
|
- name: Sign image
|
||||||
|
run: |
|
||||||
|
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
|
||||||
|
|
||||||
build_machine:
|
build_machine:
|
||||||
name: Build ${{ matrix.machine }} machine core image
|
name: Build ${{ matrix.machine }} machine core image
|
||||||
@@ -227,7 +273,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -249,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 \
|
||||||
@@ -265,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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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
|
||||||
@@ -308,13 +354,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- *install_cosign
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
|
|
||||||
- name: 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'
|
||||||
@@ -324,88 +364,104 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Meta Image
|
- name: Verify architecture image signatures
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||||
|
for arch in $ARCHS; do
|
||||||
|
echo "Verifying ${arch} image signature..."
|
||||||
|
cosign verify \
|
||||||
|
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||||
|
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||||
|
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
done
|
||||||
|
echo "✓ All images verified successfully"
|
||||||
|
|
||||||
function create_manifest() {
|
# Generate all Docker tags based on version string
|
||||||
local tag_l=${1}
|
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||||
local tag_r=${2}
|
# Examples:
|
||||||
local registry=${{ matrix.registry }}
|
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||||
|
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
||||||
|
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||||
|
- name: Generate Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||||
|
with:
|
||||||
|
images: ${{ matrix.registry }}/home-assistant
|
||||||
|
sep-tags: ","
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
||||||
|
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
||||||
|
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||||
|
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||||
|
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||||
|
type=raw,value=latest,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') }}
|
||||||
|
|
||||||
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
- name: Set up Docker Buildx
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
|
||||||
"${registry}/aarch64-homeassistant:${tag_r}"
|
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
- name: Copy architecture images to DockerHub
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
--os linux --arch amd64
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Use imagetools to copy image blobs directly between registries
|
||||||
|
# This preserves provenance/attestations and seems to be much faster than pull/push
|
||||||
|
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||||
|
for arch in $ARCHS; do
|
||||||
|
echo "Copying ${arch} image to DockerHub..."
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if docker buildx imagetools create \
|
||||||
|
--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 }}"
|
||||||
|
done
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
- name: Create and push multi-arch manifests
|
||||||
"${registry}/aarch64-homeassistant:${tag_r}" \
|
shell: bash
|
||||||
--os linux --arch arm64 --variant=v8
|
run: |
|
||||||
|
# Build list of architecture images dynamically
|
||||||
|
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||||
|
ARCH_IMAGES=()
|
||||||
|
for arch in $ARCHS; do
|
||||||
|
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
||||||
|
done
|
||||||
|
|
||||||
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
|
# Build list of all tags for single manifest creation
|
||||||
cosign sign --yes "${registry}/home-assistant:${tag_l}"
|
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||||
}
|
TAG_ARGS=()
|
||||||
|
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
||||||
|
for tag in "${TAGS[@]}"; do
|
||||||
|
TAG_ARGS+=("--tag" "${tag}")
|
||||||
|
done
|
||||||
|
|
||||||
function validate_image() {
|
# Create manifest with ALL tags in a single operation (much faster!)
|
||||||
local image=${1}
|
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||||
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
|
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||||
echo "Invalid signature!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function push_dockerhub() {
|
# Sign each tag separately (signing requires individual tag names)
|
||||||
local image=${1}
|
echo "Signing all tags..."
|
||||||
local tag=${2}
|
for tag in "${TAGS[@]}"; do
|
||||||
|
echo "Signing ${tag}"
|
||||||
|
cosign sign --yes "${tag}"
|
||||||
|
done
|
||||||
|
|
||||||
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
|
echo "All manifests created and signed successfully"
|
||||||
docker push "docker.io/homeassistant/${image}:${tag}"
|
|
||||||
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pull images from github container registry and verify signature
|
|
||||||
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
|
|
||||||
# Upload images to dockerhub
|
|
||||||
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create version tag
|
|
||||||
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
# Create general tags
|
|
||||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
|
||||||
create_manifest "dev" "${{ needs.init.outputs.version }}"
|
|
||||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
|
||||||
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
|
||||||
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
|
||||||
else
|
|
||||||
create_manifest "stable" "${{ needs.init.outputs.version }}"
|
|
||||||
create_manifest "latest" "${{ needs.init.outputs.version }}"
|
|
||||||
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
|
||||||
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
|
||||||
|
|
||||||
# Create series version tag (e.g. 2021.6)
|
|
||||||
v="${{ needs.init.outputs.version }}"
|
|
||||||
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
build_python:
|
build_python:
|
||||||
name: Build PyPi package
|
name: Build PyPi package
|
||||||
@@ -418,15 +474,15 @@ 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
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
|
||||||
|
|
||||||
@@ -463,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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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
|
||||||
@@ -495,7 +551,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
24
.github/workflows/ci.yaml
vendored
24
.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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: |
|
||||||
@@ -257,13 +257,13 @@ jobs:
|
|||||||
- &setup-python-default
|
- &setup-python-default
|
||||||
name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: &actions-setup-python actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||||
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@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
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@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
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"
|
||||||
|
|||||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
|||||||
61
.github/workflows/wheels.yml
vendored
61
.github/workflows/wheels.yml
vendored
@@ -28,16 +28,14 @@ jobs:
|
|||||||
name: Initialize wheels builder
|
name: Initialize wheels builder
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
architectures: ${{ steps.info.outputs.architectures }}
|
|
||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Checkout the repository
|
name: Checkout the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.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
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -50,10 +48,6 @@ jobs:
|
|||||||
pip install "$(grep '^uv' < requirements.txt)"
|
pip install "$(grep '^uv' < requirements.txt)"
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Get information
|
|
||||||
id: info
|
|
||||||
uses: home-assistant/actions/helpers/info@master
|
|
||||||
|
|
||||||
- name: Create requirements_diff file
|
- name: Create requirements_diff file
|
||||||
run: |
|
run: |
|
||||||
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
||||||
@@ -77,35 +71,16 @@ jobs:
|
|||||||
|
|
||||||
# Use C-Extension for SQLAlchemy
|
# Use C-Extension for SQLAlchemy
|
||||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||||
|
|
||||||
# Add additional pip wheel build constraints
|
|
||||||
echo "PIP_CONSTRAINT=build_constraints.txt"
|
|
||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Write pip wheel build constraints
|
|
||||||
run: |
|
|
||||||
(
|
|
||||||
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
|
|
||||||
# this caused the numpy builds to fail
|
|
||||||
# https://github.com/scikit-build/ninja-python-distributions/issues/274
|
|
||||||
echo "ninja==1.11.1.1"
|
|
||||||
) > build_constraints.txt
|
|
||||||
|
|
||||||
- name: Upload env_file
|
- 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
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload build_constraints
|
|
||||||
uses: *actions-upload-artifact
|
|
||||||
with:
|
|
||||||
name: build_constraints
|
|
||||||
path: ./build_constraints.txt
|
|
||||||
overwrite: true
|
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: *actions-upload-artifact
|
uses: *actions-upload-artifact
|
||||||
with:
|
with:
|
||||||
@@ -133,33 +108,21 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: &matrix-build
|
matrix: &matrix-build
|
||||||
abi: ["cp313", "cp314"]
|
abi: ["cp313", "cp314"]
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ["amd64", "aarch64"]
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- arch: amd64
|
||||||
|
os: ubuntu-latest
|
||||||
- arch: aarch64
|
- arch: aarch64
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
exclude:
|
|
||||||
- abi: cp314
|
|
||||||
arch: armv7
|
|
||||||
- abi: cp314
|
|
||||||
arch: armhf
|
|
||||||
- abi: cp314
|
|
||||||
arch: i386
|
|
||||||
steps:
|
steps:
|
||||||
- *checkout
|
- *checkout
|
||||||
|
|
||||||
- &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
|
||||||
|
|
||||||
- &download-build-constraints
|
|
||||||
name: Download build_constraints
|
|
||||||
uses: *actions-download-artifact
|
|
||||||
with:
|
|
||||||
name: build_constraints
|
|
||||||
|
|
||||||
- &download-requirements-diff
|
- &download-requirements-diff
|
||||||
name: Download requirements_diff
|
name: Download requirements_diff
|
||||||
uses: *actions-download-artifact
|
uses: *actions-download-artifact
|
||||||
@@ -172,9 +135,8 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@2025.10.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
|
||||||
@@ -199,7 +161,7 @@ jobs:
|
|||||||
- *checkout
|
- *checkout
|
||||||
|
|
||||||
- *download-env-file
|
- *download-env-file
|
||||||
- *download-build-constraints
|
|
||||||
- *download-requirements-diff
|
- *download-requirements-diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
@@ -209,17 +171,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Adjust build env
|
- name: Adjust build env
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.arch }}" = "i386" ]; then
|
|
||||||
echo "NPY_DISABLE_SVML=1" >> .env_file
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Do not pin numpy in wheels building
|
# Do not pin numpy in wheels building
|
||||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||||
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: *home-assistant-wheels
|
uses: *home-assistant-wheels
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(quality_scale)\.yaml|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(conditions|quality_scale|services|triggers)\.yaml|homeassistant/brands/.*\.json|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
||||||
- id: hassfest-metadata
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
|
files: ^(script/hassfest/(metadata|docker)\.py|homeassistant/const\.py$|pyproject\.toml)$
|
||||||
- id: hassfest-mypy-config
|
- id: hassfest-mypy-config
|
||||||
name: hassfest-mypy-config
|
name: hassfest-mypy-config
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ homeassistant.components.blueprint.*
|
|||||||
homeassistant.components.bluesound.*
|
homeassistant.components.bluesound.*
|
||||||
homeassistant.components.bluetooth.*
|
homeassistant.components.bluetooth.*
|
||||||
homeassistant.components.bluetooth_adapters.*
|
homeassistant.components.bluetooth_adapters.*
|
||||||
homeassistant.components.bluetooth_tracker.*
|
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.bosch_alarm.*
|
homeassistant.components.bosch_alarm.*
|
||||||
@@ -188,6 +187,7 @@ homeassistant.components.elkm1.*
|
|||||||
homeassistant.components.emulated_hue.*
|
homeassistant.components.emulated_hue.*
|
||||||
homeassistant.components.energenie_power_sockets.*
|
homeassistant.components.energenie_power_sockets.*
|
||||||
homeassistant.components.energy.*
|
homeassistant.components.energy.*
|
||||||
|
homeassistant.components.energyid.*
|
||||||
homeassistant.components.energyzero.*
|
homeassistant.components.energyzero.*
|
||||||
homeassistant.components.enigma2.*
|
homeassistant.components.enigma2.*
|
||||||
homeassistant.components.enphase_envoy.*
|
homeassistant.components.enphase_envoy.*
|
||||||
@@ -231,6 +231,7 @@ homeassistant.components.google_cloud.*
|
|||||||
homeassistant.components.google_drive.*
|
homeassistant.components.google_drive.*
|
||||||
homeassistant.components.google_photos.*
|
homeassistant.components.google_photos.*
|
||||||
homeassistant.components.google_sheets.*
|
homeassistant.components.google_sheets.*
|
||||||
|
homeassistant.components.google_weather.*
|
||||||
homeassistant.components.govee_ble.*
|
homeassistant.components.govee_ble.*
|
||||||
homeassistant.components.gpsd.*
|
homeassistant.components.gpsd.*
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
@@ -566,6 +567,7 @@ homeassistant.components.wake_word.*
|
|||||||
homeassistant.components.wallbox.*
|
homeassistant.components.wallbox.*
|
||||||
homeassistant.components.waqi.*
|
homeassistant.components.waqi.*
|
||||||
homeassistant.components.water_heater.*
|
homeassistant.components.water_heater.*
|
||||||
|
homeassistant.components.watts.*
|
||||||
homeassistant.components.watttime.*
|
homeassistant.components.watttime.*
|
||||||
homeassistant.components.weather.*
|
homeassistant.components.weather.*
|
||||||
homeassistant.components.webhook.*
|
homeassistant.components.webhook.*
|
||||||
@@ -578,6 +580,7 @@ homeassistant.components.wiz.*
|
|||||||
homeassistant.components.wled.*
|
homeassistant.components.wled.*
|
||||||
homeassistant.components.workday.*
|
homeassistant.components.workday.*
|
||||||
homeassistant.components.worldclock.*
|
homeassistant.components.worldclock.*
|
||||||
|
homeassistant.components.xbox.*
|
||||||
homeassistant.components.xiaomi_ble.*
|
homeassistant.components.xiaomi_ble.*
|
||||||
homeassistant.components.yale_smart_alarm.*
|
homeassistant.components.yale_smart_alarm.*
|
||||||
homeassistant.components.yalexs_ble.*
|
homeassistant.components.yalexs_ble.*
|
||||||
|
|||||||
62
CODEOWNERS
generated
62
CODEOWNERS
generated
@@ -69,8 +69,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airly/ @bieniu
|
/tests/components/airly/ @bieniu
|
||||||
/homeassistant/components/airnow/ @asymworks
|
/homeassistant/components/airnow/ @asymworks
|
||||||
/tests/components/airnow/ @asymworks
|
/tests/components/airnow/ @asymworks
|
||||||
|
/homeassistant/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
|
||||||
@@ -119,6 +123,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/androidtv/ @JeffLIrion @ollo69
|
/tests/components/androidtv/ @JeffLIrion @ollo69
|
||||||
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
|
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
|
||||||
/tests/components/androidtv_remote/ @tronikos @Drafteed
|
/tests/components/androidtv_remote/ @tronikos @Drafteed
|
||||||
|
/homeassistant/components/anglian_water/ @pantherale0
|
||||||
|
/tests/components/anglian_water/ @pantherale0
|
||||||
/homeassistant/components/anova/ @Lash-L
|
/homeassistant/components/anova/ @Lash-L
|
||||||
/tests/components/anova/ @Lash-L
|
/tests/components/anova/ @Lash-L
|
||||||
/homeassistant/components/anthemav/ @hyralex
|
/homeassistant/components/anthemav/ @hyralex
|
||||||
@@ -181,8 +187,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/automation/ @home-assistant/core
|
/homeassistant/components/automation/ @home-assistant/core
|
||||||
/tests/components/automation/ @home-assistant/core
|
/tests/components/automation/ @home-assistant/core
|
||||||
/homeassistant/components/avea/ @pattyland
|
/homeassistant/components/avea/ @pattyland
|
||||||
/homeassistant/components/awair/ @ahayworth @danielsjf
|
/homeassistant/components/awair/ @ahayworth @ricohageman
|
||||||
/tests/components/awair/ @ahayworth @danielsjf
|
/tests/components/awair/ @ahayworth @ricohageman
|
||||||
/homeassistant/components/aws_s3/ @tomasbedrich
|
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||||
/tests/components/aws_s3/ @tomasbedrich
|
/tests/components/aws_s3/ @tomasbedrich
|
||||||
/homeassistant/components/axis/ @Kane610
|
/homeassistant/components/axis/ @Kane610
|
||||||
@@ -214,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
|
||||||
@@ -302,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
|
||||||
@@ -389,6 +395,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dsmr/ @Robbie1221
|
/tests/components/dsmr/ @Robbie1221
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
|
/homeassistant/components/duckdns/ @tr4nt0r
|
||||||
|
/tests/components/duckdns/ @tr4nt0r
|
||||||
/homeassistant/components/duke_energy/ @hunterjm
|
/homeassistant/components/duke_energy/ @hunterjm
|
||||||
/tests/components/duke_energy/ @hunterjm
|
/tests/components/duke_energy/ @hunterjm
|
||||||
/homeassistant/components/duotecno/ @cereal2nd
|
/homeassistant/components/duotecno/ @cereal2nd
|
||||||
@@ -412,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
|
||||||
@@ -446,13 +456,15 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/energenie_power_sockets/ @gnumpi
|
/tests/components/energenie_power_sockets/ @gnumpi
|
||||||
/homeassistant/components/energy/ @home-assistant/core
|
/homeassistant/components/energy/ @home-assistant/core
|
||||||
/tests/components/energy/ @home-assistant/core
|
/tests/components/energy/ @home-assistant/core
|
||||||
|
/homeassistant/components/energyid/ @JrtPec @Molier
|
||||||
|
/tests/components/energyid/ @JrtPec @Molier
|
||||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||||
/tests/components/energyzero/ @klaasnicolaas
|
/tests/components/energyzero/ @klaasnicolaas
|
||||||
/homeassistant/components/enigma2/ @autinerd
|
/homeassistant/components/enigma2/ @autinerd
|
||||||
/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
|
||||||
@@ -468,6 +480,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/escea/ @lazdavila
|
/tests/components/escea/ @lazdavila
|
||||||
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
|
/homeassistant/components/essent/ @jaapp
|
||||||
|
/tests/components/essent/ @jaapp
|
||||||
/homeassistant/components/eufylife_ble/ @bdr99
|
/homeassistant/components/eufylife_ble/ @bdr99
|
||||||
/tests/components/eufylife_ble/ @bdr99
|
/tests/components/eufylife_ble/ @bdr99
|
||||||
/homeassistant/components/event/ @home-assistant/core
|
/homeassistant/components/event/ @home-assistant/core
|
||||||
@@ -529,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
|
||||||
@@ -559,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
|
||||||
@@ -587,6 +605,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/goodwe/ @mletenay @starkillerOG
|
/tests/components/goodwe/ @mletenay @starkillerOG
|
||||||
/homeassistant/components/google/ @allenporter
|
/homeassistant/components/google/ @allenporter
|
||||||
/tests/components/google/ @allenporter
|
/tests/components/google/ @allenporter
|
||||||
|
/homeassistant/components/google_air_quality/ @Thomas55555
|
||||||
|
/tests/components/google_air_quality/ @Thomas55555
|
||||||
/homeassistant/components/google_assistant/ @home-assistant/cloud
|
/homeassistant/components/google_assistant/ @home-assistant/cloud
|
||||||
/tests/components/google_assistant/ @home-assistant/cloud
|
/tests/components/google_assistant/ @home-assistant/cloud
|
||||||
/homeassistant/components/google_assistant_sdk/ @tronikos
|
/homeassistant/components/google_assistant_sdk/ @tronikos
|
||||||
@@ -607,6 +627,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/google_tasks/ @allenporter
|
/tests/components/google_tasks/ @allenporter
|
||||||
/homeassistant/components/google_travel_time/ @eifinger
|
/homeassistant/components/google_travel_time/ @eifinger
|
||||||
/tests/components/google_travel_time/ @eifinger
|
/tests/components/google_travel_time/ @eifinger
|
||||||
|
/homeassistant/components/google_weather/ @tronikos
|
||||||
|
/tests/components/google_weather/ @tronikos
|
||||||
/homeassistant/components/govee_ble/ @bdraco
|
/homeassistant/components/govee_ble/ @bdraco
|
||||||
/tests/components/govee_ble/ @bdraco
|
/tests/components/govee_ble/ @bdraco
|
||||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||||
@@ -625,6 +647,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/guardian/ @bachya
|
/tests/components/guardian/ @bachya
|
||||||
/homeassistant/components/habitica/ @tr4nt0r
|
/homeassistant/components/habitica/ @tr4nt0r
|
||||||
/tests/components/habitica/ @tr4nt0r
|
/tests/components/habitica/ @tr4nt0r
|
||||||
|
/homeassistant/components/hanna/ @bestycame
|
||||||
|
/tests/components/hanna/ @bestycame
|
||||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||||
/tests/components/hardkernel/ @home-assistant/core
|
/tests/components/hardkernel/ @home-assistant/core
|
||||||
/homeassistant/components/hardware/ @home-assistant/core
|
/homeassistant/components/hardware/ @home-assistant/core
|
||||||
@@ -640,7 +664,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/heos/ @andrewsayre
|
/tests/components/heos/ @andrewsayre
|
||||||
/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 @ptarjan
|
||||||
|
/tests/components/hikvision/ @mezz64 @ptarjan
|
||||||
/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
|
||||||
@@ -692,6 +717,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/huawei_lte/ @scop @fphammerle
|
/tests/components/huawei_lte/ @scop @fphammerle
|
||||||
/homeassistant/components/hue/ @marcelveldt
|
/homeassistant/components/hue/ @marcelveldt
|
||||||
/tests/components/hue/ @marcelveldt
|
/tests/components/hue/ @marcelveldt
|
||||||
|
/homeassistant/components/hue_ble/ @flip-dots
|
||||||
|
/tests/components/hue_ble/ @flip-dots
|
||||||
/homeassistant/components/huisbaasje/ @dennisschroer
|
/homeassistant/components/huisbaasje/ @dennisschroer
|
||||||
/tests/components/huisbaasje/ @dennisschroer
|
/tests/components/huisbaasje/ @dennisschroer
|
||||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
@@ -844,6 +871,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/kraken/ @eifinger
|
/tests/components/kraken/ @eifinger
|
||||||
/homeassistant/components/kulersky/ @emlove
|
/homeassistant/components/kulersky/ @emlove
|
||||||
/tests/components/kulersky/ @emlove
|
/tests/components/kulersky/ @emlove
|
||||||
|
/homeassistant/components/labs/ @home-assistant/core
|
||||||
|
/tests/components/labs/ @home-assistant/core
|
||||||
/homeassistant/components/lacrosse_view/ @IceBotYT
|
/homeassistant/components/lacrosse_view/ @IceBotYT
|
||||||
/tests/components/lacrosse_view/ @IceBotYT
|
/tests/components/lacrosse_view/ @IceBotYT
|
||||||
/homeassistant/components/lamarzocco/ @zweckj
|
/homeassistant/components/lamarzocco/ @zweckj
|
||||||
@@ -1166,8 +1195,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ourgroceries/ @OnFreund
|
/tests/components/ourgroceries/ @OnFreund
|
||||||
/homeassistant/components/overkiz/ @imicknl
|
/homeassistant/components/overkiz/ @imicknl
|
||||||
/tests/components/overkiz/ @imicknl
|
/tests/components/overkiz/ @imicknl
|
||||||
/homeassistant/components/overseerr/ @joostlek
|
/homeassistant/components/overseerr/ @joostlek @AmGarera
|
||||||
/tests/components/overseerr/ @joostlek
|
/tests/components/overseerr/ @joostlek @AmGarera
|
||||||
/homeassistant/components/ovo_energy/ @timmo001
|
/homeassistant/components/ovo_energy/ @timmo001
|
||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
@@ -1334,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
|
||||||
@@ -1374,6 +1403,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sanix/ @tomaszsluszniak
|
/tests/components/sanix/ @tomaszsluszniak
|
||||||
/homeassistant/components/satel_integra/ @Tommatheussen
|
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||||
/tests/components/satel_integra/ @Tommatheussen
|
/tests/components/satel_integra/ @Tommatheussen
|
||||||
|
/homeassistant/components/saunum/ @mettolen
|
||||||
|
/tests/components/saunum/ @mettolen
|
||||||
/homeassistant/components/scene/ @home-assistant/core
|
/homeassistant/components/scene/ @home-assistant/core
|
||||||
/tests/components/scene/ @home-assistant/core
|
/tests/components/scene/ @home-assistant/core
|
||||||
/homeassistant/components/schedule/ @home-assistant/core
|
/homeassistant/components/schedule/ @home-assistant/core
|
||||||
@@ -1732,11 +1763,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
|
/homeassistant/components/victron_ble/ @rajlaud
|
||||||
|
/tests/components/victron_ble/ @rajlaud
|
||||||
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
||||||
/tests/components/victron_remote_monitoring/ @AndyTempel
|
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||||
/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
|
||||||
@@ -1764,6 +1798,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/watergate/ @adam-the-hero
|
/homeassistant/components/watergate/ @adam-the-hero
|
||||||
/tests/components/watergate/ @adam-the-hero
|
/tests/components/watergate/ @adam-the-hero
|
||||||
/homeassistant/components/watson_tts/ @rutkai
|
/homeassistant/components/watson_tts/ @rutkai
|
||||||
|
/homeassistant/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
|
||||||
|
/tests/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
|
||||||
/homeassistant/components/watttime/ @bachya
|
/homeassistant/components/watttime/ @bachya
|
||||||
/tests/components/watttime/ @bachya
|
/tests/components/watttime/ @bachya
|
||||||
/homeassistant/components/waze_travel_time/ @eifinger
|
/homeassistant/components/waze_travel_time/ @eifinger
|
||||||
@@ -1776,6 +1812,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
|
||||||
|
|||||||
35
Dockerfile
generated
35
Dockerfile
generated
@@ -4,34 +4,33 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
|
LABEL \
|
||||||
|
io.hass.type="core" \
|
||||||
|
org.opencontainers.image.authors="The Home Assistant Authors" \
|
||||||
|
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
|
||||||
|
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
|
||||||
|
org.opencontainers.image.licenses="Apache-2.0" \
|
||||||
|
org.opencontainers.image.source="https://github.com/home-assistant/core" \
|
||||||
|
org.opencontainers.image.title="Home Assistant" \
|
||||||
|
org.opencontainers.image.url="https://www.home-assistant.io/"
|
||||||
|
|
||||||
# Synchronize with homeassistant/core.py:async_stop
|
# Synchronize with homeassistant/core.py:async_stop
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=240000 \
|
S6_SERVICES_GRACETIME=240000 \
|
||||||
UV_SYSTEM_PYTHON=true \
|
UV_SYSTEM_PYTHON=true \
|
||||||
UV_NO_CACHE=true
|
UV_NO_CACHE=true
|
||||||
|
|
||||||
ARG QEMU_CPU
|
|
||||||
|
|
||||||
# Home Assistant S6-Overlay
|
# Home Assistant S6-Overlay
|
||||||
COPY rootfs /
|
COPY rootfs /
|
||||||
|
|
||||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
# Add go2rtc binary
|
||||||
ARG BUILD_ARCH
|
COPY --from=ghcr.io/alexxit/go2rtc@sha256:f394f6329f5389a4c9a7fc54b09fdec9621bbb78bf7a672b973440bbdfb02241 /usr/local/bin/go2rtc /bin/go2rtc
|
||||||
# Get go2rtc binary
|
|
||||||
RUN \
|
|
||||||
case "${BUILD_ARCH}" in \
|
|
||||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
|
||||||
"armhf") go2rtc_suffix='armv6' ;; \
|
|
||||||
"armv7") go2rtc_suffix='arm' ;; \
|
|
||||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
|
||||||
esac \
|
|
||||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
|
||||||
&& chmod +x /bin/go2rtc \
|
|
||||||
# Verify go2rtc can be executed
|
|
||||||
&& go2rtc --version
|
|
||||||
|
|
||||||
# Install uv
|
RUN \
|
||||||
RUN pip3 install uv==0.9.6
|
# Verify go2rtc can be executed
|
||||||
|
go2rtc --version \
|
||||||
|
# Install uv
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
|||||||
19
build.yaml
19
build.yaml
@@ -1,19 +0,0 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
|
||||||
build_from:
|
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
|
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
|
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
|
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
|
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
|
|
||||||
cosign:
|
|
||||||
base_identity: https://github.com/home-assistant/docker/.*
|
|
||||||
identity: https://github.com/home-assistant/core/.*
|
|
||||||
labels:
|
|
||||||
io.hass.type: core
|
|
||||||
org.opencontainers.image.title: Home Assistant
|
|
||||||
org.opencontainers.image.description: Open-source home automation platform running on Python 3
|
|
||||||
org.opencontainers.image.source: https://github.com/home-assistant/core
|
|
||||||
org.opencontainers.image.authors: The Home Assistant Authors
|
|
||||||
org.opencontainers.image.url: https://www.home-assistant.io/
|
|
||||||
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
|
|
||||||
org.opencontainers.image.licenses: Apache-2.0
|
|
||||||
@@ -402,6 +402,8 @@ class AuthManager:
|
|||||||
if user.is_owner:
|
if user.is_owner:
|
||||||
raise ValueError("Unable to deactivate the owner")
|
raise ValueError("Unable to deactivate the owner")
|
||||||
await self._store.async_deactivate_user(user)
|
await self._store.async_deactivate_user(user)
|
||||||
|
for refresh_token in list(user.refresh_tokens.values()):
|
||||||
|
self.async_remove_refresh_token(refresh_token)
|
||||||
|
|
||||||
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||||
"""Remove credentials."""
|
"""Remove credentials."""
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ FRONTEND_INTEGRATIONS = {
|
|||||||
STAGE_0_INTEGRATIONS = (
|
STAGE_0_INTEGRATIONS = (
|
||||||
# Load logging and http deps as soon as possible
|
# Load logging and http deps as soon as possible
|
||||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
||||||
|
# Setup labs for preview features
|
||||||
|
("labs", {"labs"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
||||||
# Setup frontend
|
# Setup frontend
|
||||||
("frontend", FRONTEND_INTEGRATIONS, None),
|
("frontend", FRONTEND_INTEGRATIONS, None),
|
||||||
# Setup recorder
|
# Setup recorder
|
||||||
@@ -212,6 +214,7 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
"backup",
|
"backup",
|
||||||
"frontend",
|
"frontend",
|
||||||
"hardware",
|
"hardware",
|
||||||
|
"labs",
|
||||||
"logger",
|
"logger",
|
||||||
"network",
|
"network",
|
||||||
"system_health",
|
"system_health",
|
||||||
@@ -621,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
|
||||||
@@ -997,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"domain": "google",
|
"domain": "google",
|
||||||
"name": "Google",
|
"name": "Google",
|
||||||
"integrations": [
|
"integrations": [
|
||||||
|
"google_air_quality",
|
||||||
"google_assistant",
|
"google_assistant",
|
||||||
"google_assistant_sdk",
|
"google_assistant_sdk",
|
||||||
"google_cloud",
|
"google_cloud",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"google_tasks",
|
"google_tasks",
|
||||||
"google_translate",
|
"google_translate",
|
||||||
"google_travel_time",
|
"google_travel_time",
|
||||||
|
"google_weather",
|
||||||
"google_wifi",
|
"google_wifi",
|
||||||
"google",
|
"google",
|
||||||
"nest",
|
"nest",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "philips",
|
"domain": "philips",
|
||||||
"name": "Philips",
|
"name": "Philips",
|
||||||
"integrations": ["dynalite", "hue", "philips_js"]
|
"integrations": ["dynalite", "hue", "hue_ble", "philips_js"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "raspberry_pi",
|
"domain": "raspberry_pi",
|
||||||
"name": "Raspberry Pi",
|
"name": "Raspberry Pi",
|
||||||
"integrations": ["raspberry_pi", "rpi_camera", "rpi_power", "remote_rpi_gpio"]
|
"integrations": ["raspberry_pi", "rpi_power", "remote_rpi_gpio"]
|
||||||
}
|
}
|
||||||
|
|||||||
5
homeassistant/brands/victron.json
Normal file
5
homeassistant/brands/victron.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "victron",
|
||||||
|
"name": "Victron",
|
||||||
|
"integrations": ["victron_ble", "victron_remote_monitoring"]
|
||||||
|
}
|
||||||
@@ -1,40 +1,42 @@
|
|||||||
"""The Actron Air integration."""
|
"""The Actron Air integration."""
|
||||||
|
|
||||||
from actron_neo_api import (
|
from actron_neo_api import (
|
||||||
ActronAirNeoACSystem,
|
ActronAirACSystem,
|
||||||
ActronNeoAPI,
|
ActronAirAPI,
|
||||||
ActronNeoAPIError,
|
ActronAirAPIError,
|
||||||
ActronNeoAuthError,
|
ActronAirAuthError,
|
||||||
)
|
)
|
||||||
|
|
||||||
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,
|
||||||
ActronAirSystemCoordinator,
|
ActronAirSystemCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORM = [Platform.CLIMATE]
|
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
||||||
"""Set up Actron Air integration from a config entry."""
|
"""Set up Actron Air integration from a config entry."""
|
||||||
|
|
||||||
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
||||||
systems: list[ActronAirNeoACSystem] = []
|
systems: list[ActronAirACSystem] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
systems = await api.get_ac_systems()
|
systems = await api.get_ac_systems()
|
||||||
await api.update_status()
|
await api.update_status()
|
||||||
except ActronNeoAuthError:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
raise ConfigEntryAuthFailed(
|
||||||
raise
|
translation_domain=DOMAIN,
|
||||||
except ActronNeoAPIError as err:
|
translation_key="auth_error",
|
||||||
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
) from err
|
||||||
raise
|
except ActronAirAPIError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||||
for system in systems:
|
for system in systems:
|
||||||
@@ -48,10 +50,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
|||||||
system_coordinators=system_coordinators,
|
system_coordinators=system_coordinators,
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
|
from actron_neo_api import ActronAirStatus, ActronAirZone
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
@@ -132,7 +132,7 @@ class ActronSystemClimate(BaseClimateEntity):
|
|||||||
return self._status.max_temp
|
return self._status.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _status(self) -> ActronAirNeoStatus:
|
def _status(self) -> ActronAirStatus:
|
||||||
"""Get the current status from the coordinator."""
|
"""Get the current status from the coordinator."""
|
||||||
return self.coordinator.data
|
return self.coordinator.data
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ class ActronSystemClimate(BaseClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the current fan mode."""
|
"""Return the current fan mode."""
|
||||||
fan_mode = self._status.user_aircon_settings.fan_mode
|
fan_mode = self._status.user_aircon_settings.base_fan_mode
|
||||||
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
|
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -194,7 +194,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: ActronAirSystemCoordinator,
|
coordinator: ActronAirSystemCoordinator,
|
||||||
zone: ActronAirNeoZone,
|
zone: ActronAirZone,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an Actron Air unit."""
|
"""Initialize an Actron Air unit."""
|
||||||
super().__init__(coordinator, zone.title)
|
super().__init__(coordinator, zone.title)
|
||||||
@@ -221,7 +221,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
return self._zone.max_temp
|
return self._zone.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _zone(self) -> ActronAirNeoZone:
|
def _zone(self) -> ActronAirZone:
|
||||||
"""Get the current zone data from the coordinator."""
|
"""Get the current zone data from the coordinator."""
|
||||||
status = self.coordinator.data
|
status = self.coordinator.data
|
||||||
return status.zones[self._zone_id]
|
return status.zones[self._zone_id]
|
||||||
|
|||||||
@@ -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 ActronNeoAPI, ActronNeoAuthError
|
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
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the config flow."""
|
"""Initialize the config flow."""
|
||||||
self._api: ActronNeoAPI | None = None
|
self._api: ActronAirAPI | None = None
|
||||||
self._device_code: str | None = None
|
self._device_code: str | None = None
|
||||||
self._user_code: str = ""
|
self._user_code: str = ""
|
||||||
self._verification_uri: str = ""
|
self._verification_uri: str = ""
|
||||||
@@ -30,10 +31,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if self._api is None:
|
if self._api is None:
|
||||||
_LOGGER.debug("Initiating device authorization")
|
_LOGGER.debug("Initiating device authorization")
|
||||||
self._api = ActronNeoAPI()
|
self._api = ActronAirAPI()
|
||||||
try:
|
try:
|
||||||
device_code_response = await self._api.request_device_code()
|
device_code_response = await self._api.request_device_code()
|
||||||
except ActronNeoAuthError as err:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("OAuth2 flow failed: %s", err)
|
_LOGGER.error("OAuth2 flow failed: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
await self._api.poll_for_token(self._device_code)
|
await self._api.poll_for_token(self._device_code)
|
||||||
_LOGGER.debug("Authorization successful")
|
_LOGGER.debug("Authorization successful")
|
||||||
except ActronNeoAuthError as ex:
|
except ActronAirAuthError as ex:
|
||||||
_LOGGER.exception("Error while waiting for device authorization")
|
_LOGGER.exception("Error while waiting for device authorization")
|
||||||
raise CannotConnect from ex
|
raise CannotConnect from ex
|
||||||
|
|
||||||
@@ -89,14 +90,22 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user_data = await self._api.get_user_info()
|
user_data = await self._api.get_user_info()
|
||||||
except ActronNeoAuthError as err:
|
except ActronAirAuthError as err:
|
||||||
_LOGGER.error("Error getting user info: %s", err)
|
_LOGGER.error("Error getting user info: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
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 ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI
|
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"
|
||||||
|
|
||||||
@@ -23,25 +30,22 @@ ERROR_UNKNOWN = "unknown_error"
|
|||||||
class ActronAirRuntimeData:
|
class ActronAirRuntimeData:
|
||||||
"""Runtime data for the Actron Air integration."""
|
"""Runtime data for the Actron Air integration."""
|
||||||
|
|
||||||
api: ActronNeoAPI
|
api: ActronAirAPI
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
||||||
|
|
||||||
|
|
||||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
||||||
|
|
||||||
AUTH_ERROR_THRESHOLD = 3
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
|
|
||||||
"""System coordinator for Actron Air integration."""
|
"""System coordinator for Actron Air integration."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ActronAirConfigEntry,
|
entry: ActronAirConfigEntry,
|
||||||
api: ActronNeoAPI,
|
api: ActronAirAPI,
|
||||||
system: ActronAirNeoACSystem,
|
system: ActronAirACSystem,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -57,9 +61,16 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
|
|||||||
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()
|
||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirNeoStatus:
|
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
|
||||||
|
|||||||
30
homeassistant/components/actron_air/icons.json
Normal file
30
homeassistant/components/actron_air/icons.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"away_mode": {
|
||||||
|
"default": "mdi:home-export-outline",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:home-import-outline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"continuous_fan": {
|
||||||
|
"default": "mdi:fan",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:fan-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quiet_mode": {
|
||||||
|
"default": "mdi:volume-low",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:volume-high"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"turbo_mode": {
|
||||||
|
"default": "mdi:fan-plus",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:fan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.84"]
|
"requirements": ["actron-neo-api==0.4.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,39 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"away_mode": {
|
||||||
|
"name": "Away mode"
|
||||||
|
},
|
||||||
|
"continuous_fan": {
|
||||||
|
"name": "Continuous fan"
|
||||||
|
},
|
||||||
|
"quiet_mode": {
|
||||||
|
"name": "Quiet mode"
|
||||||
|
},
|
||||||
|
"turbo_mode": {
|
||||||
|
"name": "Turbo mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"auth_error": {
|
||||||
|
"message": "Authentication failed, please reauthenticate"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
homeassistant/components/actron_air/switch.py
Normal file
110
homeassistant/components/actron_air/switch.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""Switch platform for Actron Air integration."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ActronAirSwitchEntityDescription(SwitchEntityDescription):
|
||||||
|
"""Class describing Actron Air switch entities."""
|
||||||
|
|
||||||
|
is_on_fn: Callable[[ActronAirSystemCoordinator], bool]
|
||||||
|
set_fn: Callable[[ActronAirSystemCoordinator, bool], Awaitable[None]]
|
||||||
|
is_supported_fn: Callable[[ActronAirSystemCoordinator], bool] = lambda _: True
|
||||||
|
|
||||||
|
|
||||||
|
SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
|
||||||
|
ActronAirSwitchEntityDescription(
|
||||||
|
key="away_mode",
|
||||||
|
translation_key="away_mode",
|
||||||
|
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
|
||||||
|
set_fn=lambda coordinator,
|
||||||
|
enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
|
||||||
|
),
|
||||||
|
ActronAirSwitchEntityDescription(
|
||||||
|
key="continuous_fan",
|
||||||
|
translation_key="continuous_fan",
|
||||||
|
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
|
||||||
|
set_fn=lambda coordinator,
|
||||||
|
enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
|
||||||
|
),
|
||||||
|
ActronAirSwitchEntityDescription(
|
||||||
|
key="quiet_mode",
|
||||||
|
translation_key="quiet_mode",
|
||||||
|
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
|
||||||
|
set_fn=lambda coordinator,
|
||||||
|
enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
|
||||||
|
),
|
||||||
|
ActronAirSwitchEntityDescription(
|
||||||
|
key="turbo_mode",
|
||||||
|
translation_key="turbo_mode",
|
||||||
|
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
|
||||||
|
set_fn=lambda coordinator,
|
||||||
|
enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
|
||||||
|
is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ActronAirConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Actron Air switch entities."""
|
||||||
|
system_coordinators = entry.runtime_data.system_coordinators
|
||||||
|
async_add_entities(
|
||||||
|
ActronAirSwitch(coordinator, description)
|
||||||
|
for coordinator in system_coordinators.values()
|
||||||
|
for description in SWITCHES
|
||||||
|
if description.is_supported_fn(coordinator)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntity):
|
||||||
|
"""Actron Air switch."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
entity_description: ActronAirSwitchEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: ActronAirSystemCoordinator,
|
||||||
|
description: ActronAirSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the switch."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, coordinator.serial_number)},
|
||||||
|
manufacturer="Actron Air",
|
||||||
|
name=coordinator.data.ac_system.system_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the switch is on."""
|
||||||
|
return self.entity_description.is_on_fn(self.coordinator)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
await self.entity_description.set_fn(self.coordinator, True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off."""
|
||||||
|
await self.entity_description.set_fn(self.coordinator, False)
|
||||||
@@ -45,7 +45,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
|||||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
|
||||||
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adguardhome"],
|
"loggers": ["adguardhome"],
|
||||||
"requirements": ["adguardhome==0.7.0"]
|
"requirements": ["adguardhome==0.8.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
71
homeassistant/components/adguard/update.py
Normal file
71
homeassistant/components/adguard/update.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""AdGuard Home Update platform."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from adguardhome import AdGuardHomeError
|
||||||
|
|
||||||
|
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import AdGuardConfigEntry, AdGuardData
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import AdGuardHomeEntity
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=300)
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AdGuardConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AdGuard Home update entity based on a config entry."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
|
||||||
|
if (await data.client.update.update_available()).disabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
async_add_entities([AdGuardHomeUpdate(data, entry)], True)
|
||||||
|
|
||||||
|
|
||||||
|
class AdGuardHomeUpdate(AdGuardHomeEntity, UpdateEntity):
|
||||||
|
"""Defines an AdGuard Home update."""
|
||||||
|
|
||||||
|
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: AdGuardData,
|
||||||
|
entry: AdGuardConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AdGuard Home update."""
|
||||||
|
super().__init__(data, entry)
|
||||||
|
|
||||||
|
self._attr_unique_id = "_".join(
|
||||||
|
[DOMAIN, self.adguard.host, str(self.adguard.port), "update"]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _adguard_update(self) -> None:
|
||||||
|
"""Update AdGuard Home entity."""
|
||||||
|
value = await self.adguard.update.update_available()
|
||||||
|
self._attr_installed_version = self.data.version
|
||||||
|
self._attr_latest_version = value.new_version
|
||||||
|
self._attr_release_summary = value.announcement
|
||||||
|
self._attr_release_url = value.announcement_url
|
||||||
|
|
||||||
|
async def async_install(
|
||||||
|
self, version: str | None, backup: bool, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Install latest update."""
|
||||||
|
try:
|
||||||
|
await self.adguard.update.begin_update()
|
||||||
|
except AdGuardHomeError as err:
|
||||||
|
raise HomeAssistantError(f"Failed to install update: {err}") from err
|
||||||
|
self.hass.config_entries.async_schedule_reload(self._entry.entry_id)
|
||||||
@@ -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"]
|
||||||
|
|||||||
29
homeassistant/components/airobot/__init__.py
Normal file
29
homeassistant/components/airobot/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""The Airobot integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
||||||
|
"""Set up Airobot from a config entry."""
|
||||||
|
coordinator = AirobotDataUpdateCoordinator(hass, entry)
|
||||||
|
|
||||||
|
# Fetch initial data so we have data when entities subscribe
|
||||||
|
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: AirobotConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
168
homeassistant/components/airobot/climate.py
Normal file
168
homeassistant/components/airobot/climate.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"""Climate platform for Airobot thermostat."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyairobotrest.const import (
|
||||||
|
MODE_AWAY,
|
||||||
|
MODE_HOME,
|
||||||
|
SETPOINT_TEMP_MAX,
|
||||||
|
SETPOINT_TEMP_MIN,
|
||||||
|
)
|
||||||
|
from pyairobotrest.exceptions import AirobotError
|
||||||
|
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
|
||||||
|
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
PRESET_AWAY,
|
||||||
|
PRESET_BOOST,
|
||||||
|
PRESET_HOME,
|
||||||
|
ClimateEntity,
|
||||||
|
ClimateEntityFeature,
|
||||||
|
HVACAction,
|
||||||
|
HVACMode,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirobotConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import AirobotEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
_PRESET_MODE_2_MODE = {
|
||||||
|
PRESET_AWAY: MODE_AWAY,
|
||||||
|
PRESET_HOME: MODE_HOME,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirobotConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Airobot climate platform."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities([AirobotClimate(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class AirobotClimate(AirobotEntity, ClimateEntity):
|
||||||
|
"""Representation of an Airobot thermostat."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_translation_key = "thermostat"
|
||||||
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_hvac_modes = [HVACMode.HEAT]
|
||||||
|
_attr_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_BOOST]
|
||||||
|
_attr_supported_features = (
|
||||||
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||||
|
)
|
||||||
|
_attr_min_temp = SETPOINT_TEMP_MIN
|
||||||
|
_attr_max_temp = SETPOINT_TEMP_MAX
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _status(self) -> ThermostatStatus:
|
||||||
|
"""Get status from coordinator data."""
|
||||||
|
return self.coordinator.data.status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _settings(self) -> ThermostatSettings:
|
||||||
|
"""Get settings from coordinator data."""
|
||||||
|
return self.coordinator.data.settings
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> float | None:
|
||||||
|
"""Return the current temperature.
|
||||||
|
|
||||||
|
If floor temperature is available, thermostat is set up for floor heating.
|
||||||
|
"""
|
||||||
|
if self._status.temp_floor is not None:
|
||||||
|
return self._status.temp_floor
|
||||||
|
return self._status.temp_air
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_humidity(self) -> float | None:
|
||||||
|
"""Return the current humidity."""
|
||||||
|
return self._status.hum_air
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self) -> float | None:
|
||||||
|
"""Return the target temperature."""
|
||||||
|
if self._settings.is_home_mode:
|
||||||
|
return self._settings.setpoint_temp
|
||||||
|
return self._settings.setpoint_temp_away
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_mode(self) -> HVACMode:
|
||||||
|
"""Return current HVAC mode."""
|
||||||
|
if self._status.is_heating:
|
||||||
|
return HVACMode.HEAT
|
||||||
|
return HVACMode.OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hvac_action(self) -> HVACAction:
|
||||||
|
"""Return current HVAC action."""
|
||||||
|
if self._status.is_heating:
|
||||||
|
return HVACAction.HEATING
|
||||||
|
return HVACAction.IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_mode(self) -> str | None:
|
||||||
|
"""Return current preset mode."""
|
||||||
|
if self._settings.setting_flags.boost_enabled:
|
||||||
|
return PRESET_BOOST
|
||||||
|
if self._settings.is_home_mode:
|
||||||
|
return PRESET_HOME
|
||||||
|
return PRESET_AWAY
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
temperature = kwargs[ATTR_TEMPERATURE]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._settings.is_home_mode:
|
||||||
|
await self.coordinator.client.set_home_temperature(float(temperature))
|
||||||
|
else:
|
||||||
|
await self.coordinator.client.set_away_temperature(float(temperature))
|
||||||
|
except AirobotError as err:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_temperature_failed",
|
||||||
|
translation_placeholders={"temperature": str(temperature)},
|
||||||
|
) from err
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
|
"""Set HVAC mode.
|
||||||
|
|
||||||
|
This thermostat only supports HEAT mode. The climate platform validates
|
||||||
|
that only supported modes are passed, so this method is a no-op.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set new preset mode."""
|
||||||
|
try:
|
||||||
|
if preset_mode == PRESET_BOOST:
|
||||||
|
# Enable boost mode
|
||||||
|
if not self._settings.setting_flags.boost_enabled:
|
||||||
|
await self.coordinator.client.set_boost_mode(True)
|
||||||
|
else:
|
||||||
|
# Disable boost mode if it's enabled
|
||||||
|
if self._settings.setting_flags.boost_enabled:
|
||||||
|
await self.coordinator.client.set_boost_mode(False)
|
||||||
|
|
||||||
|
# Set the mode (HOME or AWAY)
|
||||||
|
await self.coordinator.client.set_mode(_PRESET_MODE_2_MODE[preset_mode])
|
||||||
|
|
||||||
|
except AirobotError as err:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_preset_mode_failed",
|
||||||
|
translation_placeholders={"preset_mode": preset_mode},
|
||||||
|
) from err
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
234
homeassistant/components/airobot/config_flow.py
Normal file
234
homeassistant/components/airobot/config_flow.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
"""Config flow for the Airobot integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyairobotrest import AirobotClient
|
||||||
|
from pyairobotrest.exceptions import (
|
||||||
|
AirobotAuthError,
|
||||||
|
AirobotConnectionError,
|
||||||
|
AirobotError,
|
||||||
|
AirobotTimeoutError,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow as BaseConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceInfo:
|
||||||
|
"""Device information."""
|
||||||
|
|
||||||
|
title: str
|
||||||
|
device_id: str
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> DeviceInfo:
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
client = AirobotClient(
|
||||||
|
host=data[CONF_HOST],
|
||||||
|
username=data[CONF_USERNAME],
|
||||||
|
password=data[CONF_PASSWORD],
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to fetch data to validate connection and authentication
|
||||||
|
status = await client.get_statuses()
|
||||||
|
settings = await client.get_settings()
|
||||||
|
except AirobotAuthError as err:
|
||||||
|
raise InvalidAuth from err
|
||||||
|
except (AirobotConnectionError, AirobotTimeoutError, AirobotError) as err:
|
||||||
|
raise CannotConnect from err
|
||||||
|
|
||||||
|
# Use device name or device ID as title
|
||||||
|
title = settings.device_name or status.device_id
|
||||||
|
|
||||||
|
return DeviceInfo(title=title, device_id=status.device_id)
|
||||||
|
|
||||||
|
|
||||||
|
class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Airobot."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self._discovered_host: str | None = None
|
||||||
|
self._discovered_mac: str | None = None
|
||||||
|
self._discovered_device_id: str | None = None
|
||||||
|
|
||||||
|
async def async_step_dhcp(
|
||||||
|
self, discovery_info: DhcpServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle DHCP discovery."""
|
||||||
|
# Store the discovered IP address and MAC
|
||||||
|
self._discovered_host = discovery_info.ip
|
||||||
|
self._discovered_mac = discovery_info.macaddress
|
||||||
|
|
||||||
|
# Extract device_id from hostname (format: airobot-thermostat-t01xxxxxx)
|
||||||
|
hostname = discovery_info.hostname.lower()
|
||||||
|
device_id = hostname.replace("airobot-thermostat-", "").upper()
|
||||||
|
self._discovered_device_id = device_id
|
||||||
|
# Set unique_id to device_id for duplicate detection
|
||||||
|
await self.async_set_unique_id(device_id)
|
||||||
|
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
|
||||||
|
|
||||||
|
# Show the confirmation form
|
||||||
|
return await self.async_step_dhcp_confirm()
|
||||||
|
|
||||||
|
async def async_step_dhcp_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle DHCP discovery confirmation - ask for credentials only."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Combine discovered host and device_id with user-provided password
|
||||||
|
data = {
|
||||||
|
CONF_HOST: self._discovered_host,
|
||||||
|
CONF_USERNAME: self._discovered_device_id,
|
||||||
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = 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:
|
||||||
|
# Store MAC address in config entry data
|
||||||
|
if self._discovered_mac:
|
||||||
|
data[CONF_MAC] = self._discovered_mac
|
||||||
|
|
||||||
|
return self.async_create_entry(title=info.title, data=data)
|
||||||
|
|
||||||
|
# Only ask for password since we already have the device_id from discovery
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="dhcp_confirm",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={
|
||||||
|
"host": self._discovered_host or "",
|
||||||
|
"device_id": self._discovered_device_id or "",
|
||||||
|
},
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
# Use device ID as unique ID to prevent duplicates
|
||||||
|
await self.async_set_unique_id(info.device_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=info.title, data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
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):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
5
homeassistant/components/airobot/const.py
Normal file
5
homeassistant/components/airobot/const.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the Airobot integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN: Final = "airobot"
|
||||||
68
homeassistant/components/airobot/coordinator.py
Normal file
68
homeassistant/components/airobot/coordinator.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""Coordinator for the Airobot integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyairobotrest import AirobotClient
|
||||||
|
from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
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
|
||||||
|
from .models import AirobotData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Update interval - thermostat measures air every 30 seconds
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
type AirobotConfigEntry = ConfigEntry[AirobotDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
|
||||||
|
"""Class to manage fetching Airobot data."""
|
||||||
|
|
||||||
|
config_entry: AirobotConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AirobotConfigEntry) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
config_entry=entry,
|
||||||
|
)
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
self.client = AirobotClient(
|
||||||
|
host=entry.data[CONF_HOST],
|
||||||
|
username=entry.data[CONF_USERNAME],
|
||||||
|
password=entry.data[CONF_PASSWORD],
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> AirobotData:
|
||||||
|
"""Fetch data from API endpoint."""
|
||||||
|
try:
|
||||||
|
status = await self.client.get_statuses()
|
||||||
|
settings = await self.client.get_settings()
|
||||||
|
except AirobotAuthError as 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)
|
||||||
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,
|
||||||
|
}
|
||||||
42
homeassistant/components/airobot/entity.py
Normal file
42
homeassistant/components/airobot/entity.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Base entity for Airobot integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_MAC
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirobotDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class AirobotEntity(CoordinatorEntity[AirobotDataUpdateCoordinator]):
|
||||||
|
"""Base class for Airobot entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirobotDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
status = coordinator.data.status
|
||||||
|
settings = coordinator.data.settings
|
||||||
|
|
||||||
|
self._attr_unique_id = status.device_id
|
||||||
|
|
||||||
|
connections = set()
|
||||||
|
if (mac := coordinator.config_entry.data.get(CONF_MAC)) is not None:
|
||||||
|
connections.add((CONNECTION_NETWORK_MAC, mac))
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, status.device_id)},
|
||||||
|
connections=connections,
|
||||||
|
name=settings.device_name or status.device_id,
|
||||||
|
manufacturer="Airobot",
|
||||||
|
model="Thermostat",
|
||||||
|
model_id="TE1",
|
||||||
|
sw_version=str(status.fw_version),
|
||||||
|
hw_version=str(status.hw_version),
|
||||||
|
)
|
||||||
17
homeassistant/components/airobot/manifest.json
Normal file
17
homeassistant/components/airobot/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"domain": "airobot",
|
||||||
|
"name": "Airobot",
|
||||||
|
"codeowners": ["@mettolen"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"hostname": "airobot-thermostat-*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/airobot",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"loggers": ["pyairobotrest"],
|
||||||
|
"quality_scale": "silver",
|
||||||
|
"requirements": ["pyairobotrest==0.1.0"]
|
||||||
|
}
|
||||||
15
homeassistant/components/airobot/models.py
Normal file
15
homeassistant/components/airobot/models.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""Models for the Airobot integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AirobotData:
|
||||||
|
"""Data from the Airobot coordinator."""
|
||||||
|
|
||||||
|
status: ThermostatStatus
|
||||||
|
settings: ThermostatSettings
|
||||||
72
homeassistant/components/airobot/quality_scale.yaml
Normal file
72
homeassistant/components/airobot/quality_scale.yaml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom actions.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not register custom actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: Integration does not use event subscriptions.
|
||||||
|
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: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: done
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: done
|
||||||
|
discovery-update-info: done
|
||||||
|
discovery: done
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Single device integration, no dynamic device discovery needed.
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: This integration doesn't have any cases where raising an issue is needed.
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: Single device integration, no stale device handling needed.
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: 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)
|
||||||
79
homeassistant/components/airobot/strings.json
Normal file
79
homeassistant/components/airobot/strings.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"dhcp_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "Device ID"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "The hostname or IP address of your Airobot thermostat.",
|
||||||
|
"password": "The thermostat password.",
|
||||||
|
"username": "The thermostat Device ID (e.g., T01XXXXXX)."
|
||||||
|
},
|
||||||
|
"description": "Enter your Airobot thermostat connection details. Find the Device ID and password in the thermostat settings menu under Connectivity → Mobile app."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"authentication_failed": {
|
||||||
|
"message": "Authentication failed, please reauthenticate."
|
||||||
|
},
|
||||||
|
"connection_failed": {
|
||||||
|
"message": "Failed to communicate with device."
|
||||||
|
},
|
||||||
|
"set_preset_mode_failed": {
|
||||||
|
"message": "Failed to set preset mode to {preset_mode}."
|
||||||
|
},
|
||||||
|
"set_temperature_failed": {
|
||||||
|
"message": "Failed to set temperature to {temperature}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
198
homeassistant/components/airpatrol/climate.py
Normal file
198
homeassistant/components/airpatrol/climate.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""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 params(self) -> dict[str, Any]:
|
||||||
|
"""Return the current parameters for the climate entity."""
|
||||||
|
return self.climate_data.get("ParametersData") or {}
|
||||||
|
|
||||||
|
@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, Platform.SENSOR]
|
||||||
|
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
|
||||||
54
homeassistant/components/airpatrol/entity.py
Normal file
54
homeassistant/components/airpatrol/entity.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""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 climate_data(self) -> dict[str, Any]:
|
||||||
|
"""Return the climate data for this unit."""
|
||||||
|
return self.device_data["climate"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return (
|
||||||
|
super().available
|
||||||
|
and self._unit_id in self.coordinator.data
|
||||||
|
and "climate" in self.device_data
|
||||||
|
and self.climate_data is not None
|
||||||
|
)
|
||||||
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
|
||||||
89
homeassistant/components/airpatrol/sensor.py
Normal file
89
homeassistant/components/airpatrol/sensor.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""Sensors for AirPatrol integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import PERCENTAGE, 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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirPatrolSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes AirPatrol sensor entity."""
|
||||||
|
|
||||||
|
data_field: str
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS = (
|
||||||
|
AirPatrolSensorEntityDescription(
|
||||||
|
key="temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
data_field="RoomTemp",
|
||||||
|
),
|
||||||
|
AirPatrolSensorEntityDescription(
|
||||||
|
key="humidity",
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
data_field="RoomHumidity",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirPatrolConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirPatrol sensors."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
units = coordinator.data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AirPatrolSensor(coordinator, unit_id, description)
|
||||||
|
for unit_id, unit in units.items()
|
||||||
|
for description in SENSOR_DESCRIPTIONS
|
||||||
|
if "climate" in unit and unit["climate"] is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirPatrolSensor(AirPatrolEntity, SensorEntity):
|
||||||
|
"""AirPatrol sensor entity."""
|
||||||
|
|
||||||
|
entity_description: AirPatrolSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirPatrolDataUpdateCoordinator,
|
||||||
|
unit_id: str,
|
||||||
|
description: AirPatrolSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize AirPatrol sensor."""
|
||||||
|
super().__init__(coordinator, unit_id)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{coordinator.config_entry.unique_id}-{unit_id}-{description.key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
if value := self.climate_data.get(self.entity_description.data_field):
|
||||||
|
return float(value)
|
||||||
|
return None
|
||||||
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"]
|
||||||
|
|||||||
@@ -36,5 +36,28 @@
|
|||||||
"alarm_trigger": {
|
"alarm_trigger": {
|
||||||
"service": "mdi:bell-ring"
|
"service": "mdi:bell-ring"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"armed": {
|
||||||
|
"trigger": "mdi:shield"
|
||||||
|
},
|
||||||
|
"armed_away": {
|
||||||
|
"trigger": "mdi:shield-lock"
|
||||||
|
},
|
||||||
|
"armed_home": {
|
||||||
|
"trigger": "mdi:shield-home"
|
||||||
|
},
|
||||||
|
"armed_night": {
|
||||||
|
"trigger": "mdi:shield-moon"
|
||||||
|
},
|
||||||
|
"armed_vacation": {
|
||||||
|
"trigger": "mdi:shield-airplane"
|
||||||
|
},
|
||||||
|
"disarmed": {
|
||||||
|
"trigger": "mdi:shield-off"
|
||||||
|
},
|
||||||
|
"triggered": {
|
||||||
|
"trigger": "mdi:bell-ring"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
||||||
|
"trigger_behavior_name": "Behavior"
|
||||||
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"action_type": {
|
"action_type": {
|
||||||
"arm_away": "Arm {entity_name} away",
|
"arm_away": "Arm {entity_name} away",
|
||||||
@@ -71,6 +75,15 @@
|
|||||||
"message": "Arming requires a code but none was given for {entity_id}."
|
"message": "Arming requires a code but none was given for {entity_id}."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"selector": {
|
||||||
|
"trigger_behavior": {
|
||||||
|
"options": {
|
||||||
|
"any": "Any",
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_arm_away": {
|
"alarm_arm_away": {
|
||||||
"description": "Arms the alarm in the away mode.",
|
"description": "Arms the alarm in the away mode.",
|
||||||
@@ -143,5 +156,77 @@
|
|||||||
"name": "Trigger"
|
"name": "Trigger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Alarm control panel"
|
"title": "Alarm control panel",
|
||||||
|
"triggers": {
|
||||||
|
"armed": {
|
||||||
|
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm armed"
|
||||||
|
},
|
||||||
|
"armed_away": {
|
||||||
|
"description": "Triggers after one or more alarms become armed in away mode.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm armed away"
|
||||||
|
},
|
||||||
|
"armed_home": {
|
||||||
|
"description": "Triggers after one or more alarms become armed in home mode.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm armed home"
|
||||||
|
},
|
||||||
|
"armed_night": {
|
||||||
|
"description": "Triggers after one or more alarms become armed in night mode.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm armed night"
|
||||||
|
},
|
||||||
|
"armed_vacation": {
|
||||||
|
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm armed vacation"
|
||||||
|
},
|
||||||
|
"disarmed": {
|
||||||
|
"description": "Triggers after one or more alarms become disarmed.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm disarmed"
|
||||||
|
},
|
||||||
|
"triggered": {
|
||||||
|
"description": "Triggers after one or more alarms become triggered.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||||
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alarm triggered"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
homeassistant/components/alarm_control_panel/trigger.py
Normal file
103
homeassistant/components/alarm_control_panel/trigger.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Provides triggers for alarm control panels."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
|
from homeassistant.helpers.trigger import (
|
||||||
|
EntityTargetStateTriggerBase,
|
||||||
|
Trigger,
|
||||||
|
make_entity_target_state_trigger,
|
||||||
|
make_entity_transition_trigger,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||||
|
|
||||||
|
|
||||||
|
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
||||||
|
"""Get the device class of an entity or UNDEFINED if not found."""
|
||||||
|
try:
|
||||||
|
return bool(get_supported_features(hass, entity_id) & features)
|
||||||
|
except HomeAssistantError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||||
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
|
_required_features: int
|
||||||
|
|
||||||
|
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||||
|
"""Filter entities of this domain."""
|
||||||
|
entities = super().entity_filter(entities)
|
||||||
|
return {
|
||||||
|
entity_id
|
||||||
|
for entity_id in entities
|
||||||
|
if supports_feature(self._hass, entity_id, self._required_features)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_entity_state_trigger_required_features(
|
||||||
|
domain: str, to_state: str, required_features: int
|
||||||
|
) -> type[EntityTargetStateTriggerBase]:
|
||||||
|
"""Create an entity state trigger class."""
|
||||||
|
|
||||||
|
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||||
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
|
_domain = domain
|
||||||
|
_to_states = {to_state}
|
||||||
|
_required_features = required_features
|
||||||
|
|
||||||
|
return CustomTrigger
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
"armed": make_entity_transition_trigger(
|
||||||
|
DOMAIN,
|
||||||
|
from_states={
|
||||||
|
AlarmControlPanelState.ARMING,
|
||||||
|
AlarmControlPanelState.DISARMED,
|
||||||
|
AlarmControlPanelState.DISARMING,
|
||||||
|
AlarmControlPanelState.PENDING,
|
||||||
|
AlarmControlPanelState.TRIGGERED,
|
||||||
|
},
|
||||||
|
to_states={
|
||||||
|
AlarmControlPanelState.ARMED_AWAY,
|
||||||
|
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||||
|
AlarmControlPanelState.ARMED_HOME,
|
||||||
|
AlarmControlPanelState.ARMED_NIGHT,
|
||||||
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"armed_away": make_entity_state_trigger_required_features(
|
||||||
|
DOMAIN,
|
||||||
|
AlarmControlPanelState.ARMED_AWAY,
|
||||||
|
AlarmControlPanelEntityFeature.ARM_AWAY,
|
||||||
|
),
|
||||||
|
"armed_home": make_entity_state_trigger_required_features(
|
||||||
|
DOMAIN,
|
||||||
|
AlarmControlPanelState.ARMED_HOME,
|
||||||
|
AlarmControlPanelEntityFeature.ARM_HOME,
|
||||||
|
),
|
||||||
|
"armed_night": make_entity_state_trigger_required_features(
|
||||||
|
DOMAIN,
|
||||||
|
AlarmControlPanelState.ARMED_NIGHT,
|
||||||
|
AlarmControlPanelEntityFeature.ARM_NIGHT,
|
||||||
|
),
|
||||||
|
"armed_vacation": make_entity_state_trigger_required_features(
|
||||||
|
DOMAIN,
|
||||||
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
|
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||||
|
),
|
||||||
|
"disarmed": make_entity_target_state_trigger(
|
||||||
|
DOMAIN, AlarmControlPanelState.DISARMED
|
||||||
|
),
|
||||||
|
"triggered": make_entity_target_state_trigger(
|
||||||
|
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for alarm control panels."""
|
||||||
|
return TRIGGERS
|
||||||
53
homeassistant/components/alarm_control_panel/triggers.yaml
Normal file
53
homeassistant/components/alarm_control_panel/triggers.yaml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
.trigger_common: &trigger_common
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: alarm_control_panel
|
||||||
|
fields: &trigger_common_fields
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
|
translation_key: trigger_behavior
|
||||||
|
|
||||||
|
armed: *trigger_common
|
||||||
|
|
||||||
|
armed_away:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: alarm_control_panel
|
||||||
|
supported_features:
|
||||||
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
||||||
|
|
||||||
|
armed_home:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: alarm_control_panel
|
||||||
|
supported_features:
|
||||||
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||||
|
|
||||||
|
armed_night:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: alarm_control_panel
|
||||||
|
supported_features:
|
||||||
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||||
|
|
||||||
|
armed_vacation:
|
||||||
|
fields: *trigger_common_fields
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: alarm_control_panel
|
||||||
|
supported_features:
|
||||||
|
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
||||||
|
|
||||||
|
disarmed: *trigger_common
|
||||||
|
|
||||||
|
triggered: *trigger_common
|
||||||
@@ -45,7 +45,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
data[CONF_PASSWORD],
|
data[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
|
|
||||||
return await api.login_mode_interactive(data[CONF_CODE])
|
return await api.login.login_mode_interactive(data[CONF_CODE])
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = 30
|
SCAN_INTERVAL = 300
|
||||||
|
|
||||||
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
name=entry.title,
|
name=entry.title,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=SCAN_INTERVAL, immediate=False
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.api = AmazonEchoApi(
|
self.api = AmazonEchoApi(
|
||||||
session,
|
session,
|
||||||
@@ -55,7 +59,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||||
"""Update device data."""
|
"""Update device data."""
|
||||||
try:
|
try:
|
||||||
await self.api.login_mode_stored_data()
|
await self.api.login.login_mode_stored_data()
|
||||||
data = await self.api.get_devices_data()
|
data = await self.api.get_devices_data()
|
||||||
except CannotConnect as err:
|
except CannotConnect as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==8.0.1"]
|
"requirements": ["aioamazondevices==10.0.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||||
from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
@@ -20,7 +18,7 @@ from .analytics import (
|
|||||||
EntityAnalyticsModifications,
|
EntityAnalyticsModifications,
|
||||||
async_devices_payload,
|
async_devices_payload,
|
||||||
)
|
)
|
||||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, PREFERENCE_SCHEMA
|
||||||
from .http import AnalyticsDevicesView
|
from .http import AnalyticsDevicesView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -31,40 +29,43 @@ __all__ = [
|
|||||||
"async_devices_payload",
|
"async_devices_payload",
|
||||||
]
|
]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONF_SNAPSHOTS_URL = "snapshots_url"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_SNAPSHOTS_URL): vol.Any(str, None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the analytics integration."""
|
"""Set up the analytics integration."""
|
||||||
analytics = Analytics(hass)
|
analytics_config = config.get(DOMAIN, {})
|
||||||
|
|
||||||
|
# For now we want to enable device analytics only if the url option
|
||||||
|
# is explicitly listed in YAML.
|
||||||
|
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||||
|
disable_snapshots = False
|
||||||
|
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||||
|
else:
|
||||||
|
disable_snapshots = True
|
||||||
|
snapshots_url = None
|
||||||
|
|
||||||
|
analytics = Analytics(hass, snapshots_url, disable_snapshots)
|
||||||
|
|
||||||
# Load stored data
|
# Load stored data
|
||||||
await analytics.load()
|
await analytics.load()
|
||||||
|
|
||||||
@callback
|
async def start_schedule(_event: Event) -> None:
|
||||||
def start_schedule(_event: Event) -> None:
|
|
||||||
"""Start the send schedule after the started event."""
|
"""Start the send schedule after the started event."""
|
||||||
# Wait 15 min after started
|
await analytics.async_schedule()
|
||||||
async_call_later(
|
|
||||||
hass,
|
|
||||||
900,
|
|
||||||
HassJob(
|
|
||||||
analytics.send_analytics,
|
|
||||||
name="analytics schedule",
|
|
||||||
cancel_on_shutdown=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send every day
|
|
||||||
async_track_time_interval(
|
|
||||||
hass,
|
|
||||||
analytics.send_analytics,
|
|
||||||
INTERVAL,
|
|
||||||
name="analytics daily",
|
|
||||||
cancel_on_shutdown=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ async def websocket_analytics_preferences(
|
|||||||
analytics = hass.data[DATA_COMPONENT]
|
analytics = hass.data[DATA_COMPONENT]
|
||||||
|
|
||||||
await analytics.save_preferences(preferences)
|
await analytics.save_preferences(preferences)
|
||||||
await analytics.send_analytics()
|
await analytics.async_schedule()
|
||||||
|
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from asyncio import timeout
|
|||||||
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||||||
from dataclasses import asdict as dataclass_asdict, dataclass, field
|
from dataclasses import asdict as dataclass_asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import random
|
||||||
|
import time
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -31,10 +33,18 @@ from homeassistant.const import (
|
|||||||
BASE_PLATFORMS,
|
BASE_PLATFORMS,
|
||||||
__version__ as HA_VERSION,
|
__version__ as HA_VERSION,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import (
|
||||||
|
CALLBACK_TYPE,
|
||||||
|
HassJob,
|
||||||
|
HomeAssistant,
|
||||||
|
ReleaseChannel,
|
||||||
|
callback,
|
||||||
|
get_release_channel,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
@@ -49,8 +59,6 @@ from homeassistant.loader import (
|
|||||||
from homeassistant.setup import async_get_loaded_integrations
|
from homeassistant.setup import async_get_loaded_integrations
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ANALYTICS_ENDPOINT_URL,
|
|
||||||
ANALYTICS_ENDPOINT_URL_DEV,
|
|
||||||
ATTR_ADDON_COUNT,
|
ATTR_ADDON_COUNT,
|
||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
@@ -71,6 +79,7 @@ from .const import (
|
|||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
ATTR_RECORDER,
|
ATTR_RECORDER,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
|
ATTR_SNAPSHOTS,
|
||||||
ATTR_STATE_COUNT,
|
ATTR_STATE_COUNT,
|
||||||
ATTR_STATISTICS,
|
ATTR_STATISTICS,
|
||||||
ATTR_SUPERVISOR,
|
ATTR_SUPERVISOR,
|
||||||
@@ -79,9 +88,15 @@ from .const import (
|
|||||||
ATTR_USER_COUNT,
|
ATTR_USER_COUNT,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
BASIC_ENDPOINT_URL,
|
||||||
|
BASIC_ENDPOINT_URL_DEV,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
INTERVAL,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
PREFERENCE_SCHEMA,
|
PREFERENCE_SCHEMA,
|
||||||
|
SNAPSHOT_DEFAULT_URL,
|
||||||
|
SNAPSHOT_URL_PATH,
|
||||||
|
SNAPSHOT_VERSION,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
@@ -194,13 +209,18 @@ def gen_uuid() -> str:
|
|||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
RELEASE_CHANNEL = get_release_channel()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AnalyticsData:
|
class AnalyticsData:
|
||||||
"""Analytics data."""
|
"""Analytics data."""
|
||||||
|
|
||||||
onboarded: bool
|
onboarded: bool
|
||||||
preferences: dict[str, bool]
|
preferences: dict[str, bool]
|
||||||
uuid: str | None
|
uuid: str | None = None
|
||||||
|
submission_identifier: str | None = None
|
||||||
|
snapshot_submission_time: float | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> AnalyticsData:
|
def from_dict(cls, data: dict[str, Any]) -> AnalyticsData:
|
||||||
@@ -209,29 +229,44 @@ class AnalyticsData:
|
|||||||
data["onboarded"],
|
data["onboarded"],
|
||||||
data["preferences"],
|
data["preferences"],
|
||||||
data["uuid"],
|
data["uuid"],
|
||||||
|
data.get("submission_identifier"),
|
||||||
|
data.get("snapshot_submission_time"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Analytics:
|
class Analytics:
|
||||||
"""Analytics helper class for the analytics integration."""
|
"""Analytics helper class for the analytics integration."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshots_url: str | None = None,
|
||||||
|
disable_snapshots: bool = False,
|
||||||
|
) -> None:
|
||||||
"""Initialize the Analytics class."""
|
"""Initialize the Analytics class."""
|
||||||
self.hass: HomeAssistant = hass
|
self._hass: HomeAssistant = hass
|
||||||
self.session = async_get_clientsession(hass)
|
self._snapshots_url = snapshots_url
|
||||||
self._data = AnalyticsData(False, {}, None)
|
self._disable_snapshots = disable_snapshots
|
||||||
|
|
||||||
|
self._session = async_get_clientsession(hass)
|
||||||
|
self._data = AnalyticsData(False, {})
|
||||||
self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
||||||
|
self._basic_scheduled: CALLBACK_TYPE | None = None
|
||||||
|
self._snapshot_scheduled: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preferences(self) -> dict:
|
def preferences(self) -> dict:
|
||||||
"""Return the current active preferences."""
|
"""Return the current active preferences."""
|
||||||
preferences = self._data.preferences
|
preferences = self._data.preferences
|
||||||
return {
|
result = {
|
||||||
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
||||||
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
||||||
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
||||||
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
||||||
}
|
}
|
||||||
|
if not self._disable_snapshots:
|
||||||
|
result[ATTR_SNAPSHOTS] = preferences.get(ATTR_SNAPSHOTS, False)
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def onboarded(self) -> bool:
|
def onboarded(self) -> bool:
|
||||||
@@ -244,17 +279,17 @@ class Analytics:
|
|||||||
return self._data.uuid
|
return self._data.uuid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self) -> str:
|
def endpoint_basic(self) -> str:
|
||||||
"""Return the endpoint that will receive the payload."""
|
"""Return the endpoint that will receive the payload."""
|
||||||
if HA_VERSION.endswith("0.dev0"):
|
if RELEASE_CHANNEL is ReleaseChannel.DEV:
|
||||||
# dev installations will contact the dev analytics environment
|
# dev installations will contact the dev analytics environment
|
||||||
return ANALYTICS_ENDPOINT_URL_DEV
|
return BASIC_ENDPOINT_URL_DEV
|
||||||
return ANALYTICS_ENDPOINT_URL
|
return BASIC_ENDPOINT_URL
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supervisor(self) -> bool:
|
def supervisor(self) -> bool:
|
||||||
"""Return bool if a supervisor is present."""
|
"""Return bool if a supervisor is present."""
|
||||||
return is_hassio(self.hass)
|
return is_hassio(self._hass)
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Load preferences."""
|
"""Load preferences."""
|
||||||
@@ -264,7 +299,7 @@ class Analytics:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
self.supervisor
|
self.supervisor
|
||||||
and (supervisor_info := hassio.get_supervisor_info(self.hass)) is not None
|
and (supervisor_info := hassio.get_supervisor_info(self._hass)) is not None
|
||||||
):
|
):
|
||||||
if not self.onboarded:
|
if not self.onboarded:
|
||||||
# User have not configured analytics, get this setting from the supervisor
|
# User have not configured analytics, get this setting from the supervisor
|
||||||
@@ -277,32 +312,35 @@ class Analytics:
|
|||||||
):
|
):
|
||||||
self._data.preferences[ATTR_DIAGNOSTICS] = False
|
self._data.preferences[ATTR_DIAGNOSTICS] = False
|
||||||
|
|
||||||
|
async def _save(self) -> None:
|
||||||
|
"""Save data."""
|
||||||
|
await self._store.async_save(dataclass_asdict(self._data))
|
||||||
|
|
||||||
async def save_preferences(self, preferences: dict) -> None:
|
async def save_preferences(self, preferences: dict) -> None:
|
||||||
"""Save preferences."""
|
"""Save preferences."""
|
||||||
preferences = PREFERENCE_SCHEMA(preferences)
|
preferences = PREFERENCE_SCHEMA(preferences)
|
||||||
self._data.preferences.update(preferences)
|
self._data.preferences.update(preferences)
|
||||||
self._data.onboarded = True
|
self._data.onboarded = True
|
||||||
|
|
||||||
await self._store.async_save(dataclass_asdict(self._data))
|
await self._save()
|
||||||
|
|
||||||
if self.supervisor:
|
if self.supervisor:
|
||||||
await hassio.async_update_diagnostics(
|
await hassio.async_update_diagnostics(
|
||||||
self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
|
self._hass, self.preferences.get(ATTR_DIAGNOSTICS, False)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_analytics(self, _: datetime | None = None) -> None:
|
async def send_analytics(self, _: datetime | None = None) -> None:
|
||||||
"""Send analytics."""
|
"""Send analytics."""
|
||||||
hass = self.hass
|
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
||||||
|
return
|
||||||
|
|
||||||
|
hass = self._hass
|
||||||
supervisor_info = None
|
supervisor_info = None
|
||||||
operating_system_info: dict[str, Any] = {}
|
operating_system_info: dict[str, Any] = {}
|
||||||
|
|
||||||
if not self.onboarded or not self.preferences.get(ATTR_BASE, False):
|
|
||||||
LOGGER.debug("Nothing to submit")
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._data.uuid is None:
|
if self._data.uuid is None:
|
||||||
self._data.uuid = gen_uuid()
|
self._data.uuid = gen_uuid()
|
||||||
await self._store.async_save(dataclass_asdict(self._data))
|
await self._save()
|
||||||
|
|
||||||
if self.supervisor:
|
if self.supervisor:
|
||||||
supervisor_info = hassio.get_supervisor_info(hass)
|
supervisor_info = hassio.get_supervisor_info(hass)
|
||||||
@@ -436,7 +474,7 @@ class Analytics:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with timeout(30):
|
async with timeout(30):
|
||||||
response = await self.session.post(self.endpoint, json=payload)
|
response = await self._session.post(self.endpoint_basic, json=payload)
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
(
|
(
|
||||||
@@ -449,14 +487,12 @@ class Analytics:
|
|||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Sending analytics failed with statuscode %s from %s",
|
"Sending analytics failed with statuscode %s from %s",
|
||||||
response.status,
|
response.status,
|
||||||
self.endpoint,
|
self.endpoint_basic,
|
||||||
)
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
|
LOGGER.error("Timeout sending analytics to %s", BASIC_ENDPOINT_URL)
|
||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
LOGGER.error(
|
LOGGER.error("Error sending analytics to %s: %r", BASIC_ENDPOINT_URL, err)
|
||||||
"Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_should_report_integration(
|
def _async_should_report_integration(
|
||||||
@@ -480,7 +516,7 @@ class Analytics:
|
|||||||
if not integration.config_flow:
|
if not integration.config_flow:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
entries = self.hass.config_entries.async_entries(integration.domain)
|
entries = self._hass.config_entries.async_entries(integration.domain)
|
||||||
|
|
||||||
# Filter out ignored and disabled entries
|
# Filter out ignored and disabled entries
|
||||||
return any(
|
return any(
|
||||||
@@ -489,6 +525,186 @@ class Analytics:
|
|||||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def send_snapshot(self, _: datetime | None = None) -> None:
|
||||||
|
"""Send a snapshot."""
|
||||||
|
if not self.onboarded or not self.preferences.get(ATTR_SNAPSHOTS, False):
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = await _async_snapshot_payload(self._hass)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": f"home-assistant/{HA_VERSION}",
|
||||||
|
}
|
||||||
|
if self._data.submission_identifier is not None:
|
||||||
|
headers["X-Device-Database-Submission-Identifier"] = (
|
||||||
|
self._data.submission_identifier
|
||||||
|
)
|
||||||
|
|
||||||
|
url = (
|
||||||
|
self._snapshots_url
|
||||||
|
if self._snapshots_url is not None
|
||||||
|
else SNAPSHOT_DEFAULT_URL
|
||||||
|
)
|
||||||
|
url += SNAPSHOT_URL_PATH
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with timeout(30):
|
||||||
|
response = await self._session.post(url, json=payload, headers=headers)
|
||||||
|
|
||||||
|
if response.status == 200: # OK
|
||||||
|
response_data = await response.json()
|
||||||
|
new_identifier = response_data.get("submission_identifier")
|
||||||
|
|
||||||
|
if (
|
||||||
|
new_identifier is not None
|
||||||
|
and new_identifier != self._data.submission_identifier
|
||||||
|
):
|
||||||
|
self._data.submission_identifier = new_identifier
|
||||||
|
await self._save()
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Submitted snapshot analytics to Home Assistant servers"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif response.status == 400: # Bad Request
|
||||||
|
response_data = await response.json()
|
||||||
|
error_kind = response_data.get("kind", "unknown")
|
||||||
|
error_message = response_data.get("message", "Unknown error")
|
||||||
|
|
||||||
|
if error_kind == "invalid-submission-identifier":
|
||||||
|
# Clear the invalid identifier and retry on next cycle
|
||||||
|
LOGGER.warning(
|
||||||
|
"Invalid submission identifier to %s, clearing: %s",
|
||||||
|
url,
|
||||||
|
error_message,
|
||||||
|
)
|
||||||
|
self._data.submission_identifier = None
|
||||||
|
await self._save()
|
||||||
|
else:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Malformed snapshot analytics submission (%s) to %s: %s",
|
||||||
|
error_kind,
|
||||||
|
url,
|
||||||
|
error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif response.status == 503: # Service Unavailable
|
||||||
|
response_text = await response.text()
|
||||||
|
LOGGER.warning(
|
||||||
|
"Snapshot analytics service %s unavailable: %s",
|
||||||
|
url,
|
||||||
|
response_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Unexpected status code %s when submitting snapshot analytics to %s",
|
||||||
|
response.status,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
|
||||||
|
except TimeoutError:
|
||||||
|
LOGGER.error(
|
||||||
|
"Timeout sending snapshot analytics to %s",
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
LOGGER.error(
|
||||||
|
"Error sending snapshot analytics to %s: %r",
|
||||||
|
url,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_schedule(self) -> None:
|
||||||
|
"""Schedule analytics."""
|
||||||
|
if not self.onboarded:
|
||||||
|
LOGGER.debug("Analytics not scheduled")
|
||||||
|
if self._basic_scheduled is not None:
|
||||||
|
self._basic_scheduled()
|
||||||
|
self._basic_scheduled = None
|
||||||
|
if self._snapshot_scheduled:
|
||||||
|
self._snapshot_scheduled()
|
||||||
|
self._snapshot_scheduled = None
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.preferences.get(ATTR_BASE, False):
|
||||||
|
LOGGER.debug("Basic analytics not scheduled")
|
||||||
|
if self._basic_scheduled is not None:
|
||||||
|
self._basic_scheduled()
|
||||||
|
self._basic_scheduled = None
|
||||||
|
elif self._basic_scheduled is None:
|
||||||
|
# Wait 15 min after started for basic analytics
|
||||||
|
self._basic_scheduled = async_call_later(
|
||||||
|
self._hass,
|
||||||
|
900,
|
||||||
|
HassJob(
|
||||||
|
self._async_schedule_basic,
|
||||||
|
name="basic analytics schedule",
|
||||||
|
cancel_on_shutdown=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.preferences.get(ATTR_SNAPSHOTS, False) or self._disable_snapshots:
|
||||||
|
LOGGER.debug("Snapshot analytics not scheduled")
|
||||||
|
if self._snapshot_scheduled:
|
||||||
|
self._snapshot_scheduled()
|
||||||
|
self._snapshot_scheduled = None
|
||||||
|
elif self._snapshot_scheduled is None:
|
||||||
|
snapshot_submission_time = self._data.snapshot_submission_time
|
||||||
|
|
||||||
|
interval_seconds = INTERVAL.total_seconds()
|
||||||
|
|
||||||
|
if snapshot_submission_time is None:
|
||||||
|
# Randomize the submission time within the 24 hours
|
||||||
|
snapshot_submission_time = random.uniform(0, interval_seconds)
|
||||||
|
self._data.snapshot_submission_time = snapshot_submission_time
|
||||||
|
await self._save()
|
||||||
|
LOGGER.debug(
|
||||||
|
"Initialized snapshot submission time to %s",
|
||||||
|
snapshot_submission_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate delay until next submission
|
||||||
|
current_time = time.time()
|
||||||
|
delay = (snapshot_submission_time - current_time) % interval_seconds
|
||||||
|
|
||||||
|
self._snapshot_scheduled = async_call_later(
|
||||||
|
self._hass,
|
||||||
|
delay,
|
||||||
|
HassJob(
|
||||||
|
self._async_schedule_snapshots,
|
||||||
|
name="snapshot analytics schedule",
|
||||||
|
cancel_on_shutdown=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_schedule_basic(self, _: datetime | None = None) -> None:
|
||||||
|
"""Schedule basic analytics."""
|
||||||
|
await self.send_analytics()
|
||||||
|
|
||||||
|
# Send basic analytics every day
|
||||||
|
self._basic_scheduled = async_track_time_interval(
|
||||||
|
self._hass,
|
||||||
|
self.send_analytics,
|
||||||
|
INTERVAL,
|
||||||
|
name="basic analytics daily",
|
||||||
|
cancel_on_shutdown=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_schedule_snapshots(self, _: datetime | None = None) -> None:
|
||||||
|
"""Schedule snapshot analytics."""
|
||||||
|
await self.send_snapshot()
|
||||||
|
|
||||||
|
# Send snapshot analytics every day
|
||||||
|
self._snapshot_scheduled = async_track_time_interval(
|
||||||
|
self._hass,
|
||||||
|
self.send_snapshot,
|
||||||
|
INTERVAL,
|
||||||
|
name="snapshot analytics daily",
|
||||||
|
cancel_on_shutdown=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||||
"""Extract domains from the YAML configuration."""
|
"""Extract domains from the YAML configuration."""
|
||||||
@@ -505,8 +721,8 @@ DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
|
|||||||
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
|
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
|
||||||
|
|
||||||
|
|
||||||
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
async def _async_snapshot_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||||
"""Return detailed information about entities and devices."""
|
"""Return detailed information about entities and devices for a snapshot."""
|
||||||
dev_reg = dr.async_get(hass)
|
dev_reg = dr.async_get(hass)
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
@@ -711,8 +927,13 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
|||||||
|
|
||||||
entities_info.append(entity_info)
|
entities_info.append(entity_info)
|
||||||
|
|
||||||
|
return integrations_info
|
||||||
|
|
||||||
|
|
||||||
|
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||||
|
"""Return detailed information about entities and devices for a direct download."""
|
||||||
return {
|
return {
|
||||||
"version": "home-assistant:1",
|
"version": f"home-assistant:{SNAPSHOT_VERSION}",
|
||||||
"home_assistant": HA_VERSION,
|
"home_assistant": HA_VERSION,
|
||||||
"integrations": integrations_info,
|
"integrations": await _async_snapshot_payload(hass),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
|
|
||||||
ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1"
|
|
||||||
DOMAIN = "analytics"
|
DOMAIN = "analytics"
|
||||||
INTERVAL = timedelta(days=1)
|
INTERVAL = timedelta(days=1)
|
||||||
STORAGE_KEY = "core.analytics"
|
STORAGE_KEY = "core.analytics"
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
|
|
||||||
|
BASIC_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1"
|
||||||
|
BASIC_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1"
|
||||||
|
|
||||||
|
SNAPSHOT_VERSION = 1
|
||||||
|
SNAPSHOT_DEFAULT_URL = "https://device-database.eco-dev-aws.openhomefoundation.com"
|
||||||
|
SNAPSHOT_URL_PATH = f"/api/v1/snapshot/{SNAPSHOT_VERSION}"
|
||||||
|
|
||||||
LOGGER: logging.Logger = logging.getLogger(__package__)
|
LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@ ATTR_PREFERENCES = "preferences"
|
|||||||
ATTR_PROTECTED = "protected"
|
ATTR_PROTECTED = "protected"
|
||||||
ATTR_RECORDER = "recorder"
|
ATTR_RECORDER = "recorder"
|
||||||
ATTR_SLUG = "slug"
|
ATTR_SLUG = "slug"
|
||||||
|
ATTR_SNAPSHOTS = "snapshots"
|
||||||
ATTR_STATE_COUNT = "state_count"
|
ATTR_STATE_COUNT = "state_count"
|
||||||
ATTR_STATISTICS = "statistics"
|
ATTR_STATISTICS = "statistics"
|
||||||
ATTR_SUPERVISOR = "supervisor"
|
ATTR_SUPERVISOR = "supervisor"
|
||||||
@@ -51,6 +56,7 @@ ATTR_VERSION = "version"
|
|||||||
PREFERENCE_SCHEMA = vol.Schema(
|
PREFERENCE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_BASE): bool,
|
vol.Optional(ATTR_BASE): bool,
|
||||||
|
vol.Optional(ATTR_SNAPSHOTS): bool,
|
||||||
vol.Optional(ATTR_DIAGNOSTICS): bool,
|
vol.Optional(ATTR_DIAGNOSTICS): bool,
|
||||||
vol.Optional(ATTR_STATISTICS): bool,
|
vol.Optional(ATTR_STATISTICS): bool,
|
||||||
vol.Optional(ATTR_USAGE): bool,
|
vol.Optional(ATTR_USAGE): bool,
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
73
homeassistant/components/anglian_water/__init__.py
Normal file
73
homeassistant/components/anglian_water/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""The Anglian Water integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aiohttp import CookieJar
|
||||||
|
from pyanglianwater import AnglianWater
|
||||||
|
from pyanglianwater.auth import MSOB2CAuth
|
||||||
|
from pyanglianwater.exceptions import (
|
||||||
|
ExpiredAccessTokenError,
|
||||||
|
SelfAssertedError,
|
||||||
|
SmartMeterUnavailableError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
|
||||||
|
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
||||||
|
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
|
||||||
|
|
||||||
|
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: AnglianWaterConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up Anglian Water from a config entry."""
|
||||||
|
auth = MSOB2CAuth(
|
||||||
|
username=entry.data[CONF_USERNAME],
|
||||||
|
password=entry.data[CONF_PASSWORD],
|
||||||
|
session=async_create_clientsession(
|
||||||
|
hass,
|
||||||
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
|
),
|
||||||
|
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await auth.send_refresh_request()
|
||||||
|
except (ExpiredAccessTokenError, SelfAssertedError) as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
|
|
||||||
|
_aw = AnglianWater(authenticator=auth)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _aw.validate_smart_meter(entry.data[CONF_ACCOUNT_NUMBER])
|
||||||
|
except SmartMeterUnavailableError as err:
|
||||||
|
raise ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, data={**entry.data, CONF_ACCESS_TOKEN: auth.refresh_token}
|
||||||
|
)
|
||||||
|
entry.runtime_data = coordinator = AnglianWaterUpdateCoordinator(
|
||||||
|
hass=hass, api=_aw, config_entry=entry
|
||||||
|
)
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AnglianWaterConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
92
homeassistant/components/anglian_water/config_flow.py
Normal file
92
homeassistant/components/anglian_water/config_flow.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""Config flow for the Anglian Water integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import CookieJar
|
||||||
|
from pyanglianwater import AnglianWater
|
||||||
|
from pyanglianwater.auth import MSOB2CAuth
|
||||||
|
from pyanglianwater.exceptions import (
|
||||||
|
InvalidAccountIdError,
|
||||||
|
SelfAssertedError,
|
||||||
|
SmartMeterUnavailableError,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
|
||||||
|
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USERNAME): selector.TextSelector(),
|
||||||
|
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
||||||
|
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_credentials(
|
||||||
|
auth: MSOB2CAuth, account_number: str
|
||||||
|
) -> str | MSOB2CAuth:
|
||||||
|
"""Validate the provided credentials."""
|
||||||
|
try:
|
||||||
|
await auth.send_login_request()
|
||||||
|
except SelfAssertedError:
|
||||||
|
return "invalid_auth"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
return "unknown"
|
||||||
|
_aw = AnglianWater(authenticator=auth)
|
||||||
|
try:
|
||||||
|
await _aw.validate_smart_meter(account_number)
|
||||||
|
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
||||||
|
return "smart_meter_unavailable"
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Anglian Water."""
|
||||||
|
|
||||||
|
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:
|
||||||
|
validation_response = await validate_credentials(
|
||||||
|
MSOB2CAuth(
|
||||||
|
username=user_input[CONF_USERNAME],
|
||||||
|
password=user_input[CONF_PASSWORD],
|
||||||
|
session=async_create_clientsession(
|
||||||
|
self.hass,
|
||||||
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
user_input[CONF_ACCOUNT_NUMBER],
|
||||||
|
)
|
||||||
|
if isinstance(validation_response, str):
|
||||||
|
errors["base"] = validation_response
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_ACCOUNT_NUMBER],
|
||||||
|
data={
|
||||||
|
**user_input,
|
||||||
|
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
4
homeassistant/components/anglian_water/const.py
Normal file
4
homeassistant/components/anglian_water/const.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Constants for the Anglian Water integration."""
|
||||||
|
|
||||||
|
DOMAIN = "anglian_water"
|
||||||
|
CONF_ACCOUNT_NUMBER = "account_number"
|
||||||
165
homeassistant/components/anglian_water/coordinator.py
Normal file
165
homeassistant/components/anglian_water/coordinator.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""Anglian Water data coordinator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyanglianwater import AnglianWater
|
||||||
|
from pyanglianwater.exceptions import ExpiredAccessTokenError, UnknownEndpointError
|
||||||
|
|
||||||
|
from homeassistant.components.recorder import get_instance
|
||||||
|
from homeassistant.components.recorder.models import (
|
||||||
|
StatisticData,
|
||||||
|
StatisticMeanType,
|
||||||
|
StatisticMetaData,
|
||||||
|
)
|
||||||
|
from homeassistant.components.recorder.statistics import (
|
||||||
|
async_add_external_statistics,
|
||||||
|
get_last_statistics,
|
||||||
|
statistics_during_period,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import UnitOfVolume
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util.unit_conversion import VolumeConverter
|
||||||
|
|
||||||
|
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
||||||
|
|
||||||
|
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||||
|
|
||||||
|
|
||||||
|
class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""Anglian Water data update coordinator."""
|
||||||
|
|
||||||
|
config_entry: AnglianWaterConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
api: AnglianWater,
|
||||||
|
config_entry: AnglianWaterConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize update coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Update data from Anglian Water's API."""
|
||||||
|
try:
|
||||||
|
await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
|
||||||
|
await self._insert_statistics()
|
||||||
|
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
|
||||||
|
raise UpdateFailed from err
|
||||||
|
|
||||||
|
async def _insert_statistics(self) -> None:
|
||||||
|
"""Insert statistics for water meters into Home Assistant."""
|
||||||
|
for meter in self.api.meters.values():
|
||||||
|
id_prefix = (
|
||||||
|
f"{self.config_entry.data[CONF_ACCOUNT_NUMBER]}_{meter.serial_number}"
|
||||||
|
)
|
||||||
|
usage_statistic_id = f"{DOMAIN}:{id_prefix}_usage".lower()
|
||||||
|
_LOGGER.debug("Updating statistics for meter %s", meter.serial_number)
|
||||||
|
name_prefix = (
|
||||||
|
f"Anglian Water {self.config_entry.data[CONF_ACCOUNT_NUMBER]} "
|
||||||
|
f"{meter.serial_number}"
|
||||||
|
)
|
||||||
|
usage_metadata = StatisticMetaData(
|
||||||
|
mean_type=StatisticMeanType.NONE,
|
||||||
|
has_sum=True,
|
||||||
|
name=f"{name_prefix} Usage",
|
||||||
|
source=DOMAIN,
|
||||||
|
statistic_id=usage_statistic_id,
|
||||||
|
unit_class=VolumeConverter.UNIT_CLASS,
|
||||||
|
unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||||
|
)
|
||||||
|
last_stat = await get_instance(self.hass).async_add_executor_job(
|
||||||
|
get_last_statistics, self.hass, 1, usage_statistic_id, True, set()
|
||||||
|
)
|
||||||
|
if not last_stat:
|
||||||
|
_LOGGER.debug("Updating statistics for the first time")
|
||||||
|
usage_sum = 0.0
|
||||||
|
last_stats_time = None
|
||||||
|
else:
|
||||||
|
if not meter.readings or len(meter.readings) == 0:
|
||||||
|
_LOGGER.debug("No recent usage statistics found, skipping update")
|
||||||
|
continue
|
||||||
|
# Anglian Water stats are hourly, the read_at time is the time that the meter took the reading
|
||||||
|
# We remove 1 hour from this so that the data is shown in the correct hour on the dashboards
|
||||||
|
parsed_read_at = dt_util.parse_datetime(meter.readings[0]["read_at"])
|
||||||
|
if not parsed_read_at:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Could not parse read_at time %s, skipping update",
|
||||||
|
meter.readings[0]["read_at"],
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||||
|
_LOGGER.debug("Getting statistics at %s", start)
|
||||||
|
for end in (start + timedelta(seconds=1), None):
|
||||||
|
stats = await get_instance(self.hass).async_add_executor_job(
|
||||||
|
statistics_during_period,
|
||||||
|
self.hass,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
{
|
||||||
|
usage_statistic_id,
|
||||||
|
},
|
||||||
|
"hour",
|
||||||
|
None,
|
||||||
|
{"sum"},
|
||||||
|
)
|
||||||
|
if stats:
|
||||||
|
break
|
||||||
|
if end:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Not found, trying to find oldest statistic after %s",
|
||||||
|
start,
|
||||||
|
)
|
||||||
|
assert stats
|
||||||
|
|
||||||
|
def _safe_get_sum(records: list[Any]) -> float:
|
||||||
|
if records and "sum" in records[0]:
|
||||||
|
return float(records[0]["sum"])
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
|
||||||
|
last_stats_time = stats[usage_statistic_id][0]["start"]
|
||||||
|
|
||||||
|
usage_statistics = []
|
||||||
|
|
||||||
|
for read in meter.readings:
|
||||||
|
parsed_read_at = dt_util.parse_datetime(read["read_at"])
|
||||||
|
if not parsed_read_at:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Could not parse read_at time %s, skipping reading",
|
||||||
|
read["read_at"],
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||||
|
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||||
|
continue
|
||||||
|
usage_state = max(0, read["consumption"] / 1000)
|
||||||
|
usage_sum = max(0, read["read"])
|
||||||
|
usage_statistics.append(
|
||||||
|
StatisticData(
|
||||||
|
start=start,
|
||||||
|
state=usage_state,
|
||||||
|
sum=usage_sum,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Adding %s statistics for %s", len(usage_statistics), usage_statistic_id
|
||||||
|
)
|
||||||
|
async_add_external_statistics(self.hass, usage_metadata, usage_statistics)
|
||||||
48
homeassistant/components/anglian_water/entity.py
Normal file
48
homeassistant/components/anglian_water/entity.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Anglian Water entity."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyanglianwater.meter import SmartMeter
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AnglianWaterUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
|
||||||
|
"""Defines a Anglian Water entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AnglianWaterUpdateCoordinator,
|
||||||
|
smart_meter: SmartMeter,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Anglian Water entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.smart_meter = smart_meter
|
||||||
|
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, smart_meter.serial_number)},
|
||||||
|
name=smart_meter.serial_number,
|
||||||
|
manufacturer="Anglian Water",
|
||||||
|
serial_number=smart_meter.serial_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""When entity is loaded."""
|
||||||
|
self.coordinator.api.updated_data_callbacks.append(self.async_write_ha_state)
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""When will be removed from HASS."""
|
||||||
|
self.coordinator.api.updated_data_callbacks.remove(self.async_write_ha_state)
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
13
homeassistant/components/anglian_water/manifest.json
Normal file
13
homeassistant/components/anglian_water/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"domain": "anglian_water",
|
||||||
|
"name": "Anglian Water",
|
||||||
|
"after_dependencies": ["recorder"],
|
||||||
|
"codeowners": ["@pantherale0"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
||||||
|
"integration_type": "service",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["pyanglianwater"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["pyanglianwater==3.1.0"]
|
||||||
|
}
|
||||||
83
homeassistant/components/anglian_water/quality_scale.yaml
Normal file
83
homeassistant/components/anglian_water/quality_scale.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup: done
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: todo
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Unable to discover meters.
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Unable to discover meters.
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No entities are disabled by default.
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities do not require different icons.
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Read-only integration and no repairs are possible.
|
||||||
|
stale-devices: todo
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: todo
|
||||||
117
homeassistant/components/anglian_water/sensor.py
Normal file
117
homeassistant/components/anglian_water/sensor.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""Anglian Water sensor platform."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pyanglianwater.meter import SmartMeter
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
EntityCategory,
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfVolume
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
|
||||||
|
from .entity import AnglianWaterEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
class AnglianWaterSensor(StrEnum):
|
||||||
|
"""Store keys for Anglian Water sensors."""
|
||||||
|
|
||||||
|
YESTERDAY_CONSUMPTION = "yesterday_consumption"
|
||||||
|
YESTERDAY_WATER_COST = "yesterday_water_cost"
|
||||||
|
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
|
||||||
|
LATEST_READING = "latest_reading"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes AnglianWater sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[SmartMeter], float]
|
||||||
|
|
||||||
|
|
||||||
|
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
||||||
|
AnglianWaterSensorEntityDescription(
|
||||||
|
key=AnglianWaterSensor.YESTERDAY_CONSUMPTION,
|
||||||
|
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||||
|
device_class=SensorDeviceClass.WATER,
|
||||||
|
value_fn=lambda entity: entity.get_yesterday_consumption,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
translation_key=AnglianWaterSensor.YESTERDAY_CONSUMPTION,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
AnglianWaterSensorEntityDescription(
|
||||||
|
key=AnglianWaterSensor.LATEST_READING,
|
||||||
|
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||||
|
device_class=SensorDeviceClass.WATER,
|
||||||
|
value_fn=lambda entity: entity.latest_read,
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
translation_key=AnglianWaterSensor.LATEST_READING,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
AnglianWaterSensorEntityDescription(
|
||||||
|
key=AnglianWaterSensor.YESTERDAY_WATER_COST,
|
||||||
|
native_unit_of_measurement="GBP",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
value_fn=lambda entity: entity.yesterday_water_cost,
|
||||||
|
translation_key=AnglianWaterSensor.YESTERDAY_WATER_COST,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
AnglianWaterSensorEntityDescription(
|
||||||
|
key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
|
||||||
|
native_unit_of_measurement="GBP",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
value_fn=lambda entity: entity.yesterday_sewerage_cost,
|
||||||
|
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AnglianWaterConfigEntry,
|
||||||
|
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensor platform."""
|
||||||
|
async_add_devices(
|
||||||
|
AnglianWaterSensorEntity(
|
||||||
|
coordinator=entry.runtime_data,
|
||||||
|
description=entity_description,
|
||||||
|
smart_meter=smart_meter,
|
||||||
|
)
|
||||||
|
for entity_description in ENTITY_DESCRIPTIONS
|
||||||
|
for smart_meter in entry.runtime_data.api.meters.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
|
||||||
|
"""Defines a Anglian Water sensor."""
|
||||||
|
|
||||||
|
entity_description: AnglianWaterSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AnglianWaterUpdateCoordinator,
|
||||||
|
smart_meter: SmartMeter,
|
||||||
|
description: AnglianWaterSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Anglian Water sensor."""
|
||||||
|
super().__init__(coordinator, smart_meter, description.key)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.smart_meter)
|
||||||
55
homeassistant/components/anglian_water/strings.json
Normal file
55
homeassistant/components/anglian_water/strings.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"smart_meter_unavailable": "This account does not have any smart meters associated with it. If this is unexpected, enter your Billing Account Number found at the top of your latest bill.",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"account_number": "Billing Account Number",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"account_number": "Your account number found on your latest bill.",
|
||||||
|
"password": "Your password",
|
||||||
|
"username": "Username or email used to log in to the Anglian Water website."
|
||||||
|
},
|
||||||
|
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"latest_reading": {
|
||||||
|
"name": "Latest reading"
|
||||||
|
},
|
||||||
|
"yesterday_consumption": {
|
||||||
|
"name": "Yesterday's usage"
|
||||||
|
},
|
||||||
|
"yesterday_sewerage_cost": {
|
||||||
|
"name": "Yesterday's sewerage cost"
|
||||||
|
},
|
||||||
|
"yesterday_water_cost": {
|
||||||
|
"name": "Yesterday's water cost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"auth_expired": {
|
||||||
|
"message": "Authentication token expired"
|
||||||
|
},
|
||||||
|
"service_unavailable": {
|
||||||
|
"message": "Anglian Water services are currently unavailable for maintenance."
|
||||||
|
},
|
||||||
|
"smart_meter_unavailable": {
|
||||||
|
"message": "This account no longer has a smart meter associated with it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -17,13 +17,7 @@ from homeassistant.helpers import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||||
CONF_CHAT_MODEL,
|
|
||||||
DEFAULT_CONVERSATION_NAME,
|
|
||||||
DOMAIN,
|
|
||||||
LOGGER,
|
|
||||||
RECOMMENDED_CHAT_MODEL,
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
@@ -43,14 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
|||||||
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
|
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
# Use model from first conversation subentry for validation
|
await client.models.list(timeout=10.0)
|
||||||
subentries = list(entry.subentries.values())
|
|
||||||
if subentries:
|
|
||||||
model_id = subentries[0].data.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
|
||||||
else:
|
|
||||||
model_id = RECOMMENDED_CHAT_MODEL
|
|
||||||
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
|
|
||||||
LOGGER.debug("Anthropic model: %s", model.display_name)
|
|
||||||
except anthropic.AuthenticationError as err:
|
except anthropic.AuthenticationError as err:
|
||||||
LOGGER.error("Invalid API key: %s", err)
|
LOGGER.error("Invalid API key: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ from __future__ import annotations
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
import re
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import anthropic
|
import anthropic
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -53,17 +54,11 @@ from .const import (
|
|||||||
CONF_WEB_SEARCH_REGION,
|
CONF_WEB_SEARCH_REGION,
|
||||||
CONF_WEB_SEARCH_TIMEZONE,
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
CONF_WEB_SEARCH_USER_LOCATION,
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
|
DEFAULT,
|
||||||
DEFAULT_AI_TASK_NAME,
|
DEFAULT_AI_TASK_NAME,
|
||||||
DEFAULT_CONVERSATION_NAME,
|
DEFAULT_CONVERSATION_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
NON_THINKING_MODELS,
|
NON_THINKING_MODELS,
|
||||||
RECOMMENDED_CHAT_MODEL,
|
|
||||||
RECOMMENDED_MAX_TOKENS,
|
|
||||||
RECOMMENDED_TEMPERATURE,
|
|
||||||
RECOMMENDED_THINKING_BUDGET,
|
|
||||||
RECOMMENDED_WEB_SEARCH,
|
|
||||||
RECOMMENDED_WEB_SEARCH_MAX_USES,
|
|
||||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
|
||||||
WEB_SEARCH_UNSUPPORTED_MODELS,
|
WEB_SEARCH_UNSUPPORTED_MODELS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,13 +70,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
RECOMMENDED_CONVERSATION_OPTIONS = {
|
DEFAULT_CONVERSATION_OPTIONS = {
|
||||||
CONF_RECOMMENDED: True,
|
CONF_RECOMMENDED: True,
|
||||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
||||||
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
||||||
}
|
}
|
||||||
|
|
||||||
RECOMMENDED_AI_TASK_OPTIONS = {
|
DEFAULT_AI_TASK_OPTIONS = {
|
||||||
CONF_RECOMMENDED: True,
|
CONF_RECOMMENDED: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,13 +130,13 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
subentries=[
|
subentries=[
|
||||||
{
|
{
|
||||||
"subentry_type": "conversation",
|
"subentry_type": "conversation",
|
||||||
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
"data": DEFAULT_CONVERSATION_OPTIONS,
|
||||||
"title": DEFAULT_CONVERSATION_NAME,
|
"title": DEFAULT_CONVERSATION_NAME,
|
||||||
"unique_id": None,
|
"unique_id": None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subentry_type": "ai_task_data",
|
"subentry_type": "ai_task_data",
|
||||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
"data": DEFAULT_AI_TASK_OPTIONS,
|
||||||
"title": DEFAULT_AI_TASK_NAME,
|
"title": DEFAULT_AI_TASK_NAME,
|
||||||
"unique_id": None,
|
"unique_id": None,
|
||||||
},
|
},
|
||||||
@@ -179,9 +174,9 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
) -> SubentryFlowResult:
|
) -> SubentryFlowResult:
|
||||||
"""Add a subentry."""
|
"""Add a subentry."""
|
||||||
if self._subentry_type == "ai_task_data":
|
if self._subentry_type == "ai_task_data":
|
||||||
self.options = RECOMMENDED_AI_TASK_OPTIONS.copy()
|
self.options = DEFAULT_AI_TASK_OPTIONS.copy()
|
||||||
else:
|
else:
|
||||||
self.options = RECOMMENDED_CONVERSATION_OPTIONS.copy()
|
self.options = DEFAULT_CONVERSATION_OPTIONS.copy()
|
||||||
return await self.async_step_init()
|
return await self.async_step_init()
|
||||||
|
|
||||||
async def async_step_reconfigure(
|
async def async_step_reconfigure(
|
||||||
@@ -282,15 +277,19 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
step_schema: VolDictType = {
|
step_schema: VolDictType = {
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
default=RECOMMENDED_CHAT_MODEL,
|
default=DEFAULT[CONF_CHAT_MODEL],
|
||||||
): str,
|
): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=await self._get_model_list(), custom_value=True
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
default=RECOMMENDED_MAX_TOKENS,
|
default=DEFAULT[CONF_MAX_TOKENS],
|
||||||
): int,
|
): int,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
default=RECOMMENDED_TEMPERATURE,
|
default=DEFAULT[CONF_TEMPERATURE],
|
||||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +319,14 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
|
|
||||||
if not model.startswith(tuple(NON_THINKING_MODELS)):
|
if not model.startswith(tuple(NON_THINKING_MODELS)):
|
||||||
step_schema[
|
step_schema[
|
||||||
vol.Optional(CONF_THINKING_BUDGET, default=RECOMMENDED_THINKING_BUDGET)
|
vol.Optional(
|
||||||
|
CONF_THINKING_BUDGET, default=DEFAULT[CONF_THINKING_BUDGET]
|
||||||
|
)
|
||||||
] = vol.All(
|
] = vol.All(
|
||||||
NumberSelector(
|
NumberSelector(
|
||||||
NumberSelectorConfig(
|
NumberSelectorConfig(
|
||||||
min=0,
|
min=0,
|
||||||
max=self.options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
max=self.options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
@@ -338,15 +339,15 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_WEB_SEARCH,
|
CONF_WEB_SEARCH,
|
||||||
default=RECOMMENDED_WEB_SEARCH,
|
default=DEFAULT[CONF_WEB_SEARCH],
|
||||||
): bool,
|
): bool,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_WEB_SEARCH_MAX_USES,
|
CONF_WEB_SEARCH_MAX_USES,
|
||||||
default=RECOMMENDED_WEB_SEARCH_MAX_USES,
|
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
|
||||||
): int,
|
): int,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_WEB_SEARCH_USER_LOCATION,
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
|
||||||
): bool,
|
): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -364,9 +365,10 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
user_input = {}
|
user_input = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if user_input.get(CONF_WEB_SEARCH, RECOMMENDED_WEB_SEARCH) and not errors:
|
if user_input.get(CONF_WEB_SEARCH, DEFAULT[CONF_WEB_SEARCH]) and not errors:
|
||||||
if user_input.get(
|
if user_input.get(
|
||||||
CONF_WEB_SEARCH_USER_LOCATION, RECOMMENDED_WEB_SEARCH_USER_LOCATION
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
|
DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
|
||||||
):
|
):
|
||||||
user_input.update(await self._get_location_data())
|
user_input.update(await self._get_location_data())
|
||||||
|
|
||||||
@@ -394,6 +396,41 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
last_step=True,
|
last_step=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _get_model_list(self) -> list[SelectOptionDict]:
|
||||||
|
"""Get list of available models."""
|
||||||
|
try:
|
||||||
|
client = await self.hass.async_add_executor_job(
|
||||||
|
partial(
|
||||||
|
anthropic.AsyncAnthropic,
|
||||||
|
api_key=self._get_entry().data[CONF_API_KEY],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
models = (await client.models.list()).data
|
||||||
|
except anthropic.AnthropicError:
|
||||||
|
models = []
|
||||||
|
_LOGGER.debug("Available models: %s", models)
|
||||||
|
model_options: list[SelectOptionDict] = []
|
||||||
|
short_form = re.compile(r"[^\d]-\d$")
|
||||||
|
for model_info in models:
|
||||||
|
# Resolve alias from versioned model name:
|
||||||
|
model_alias = (
|
||||||
|
model_info.id[:-9]
|
||||||
|
if model_info.id
|
||||||
|
not in ("claude-3-haiku-20240307", "claude-3-opus-20240229")
|
||||||
|
else model_info.id
|
||||||
|
)
|
||||||
|
if short_form.search(model_alias):
|
||||||
|
model_alias += "-0"
|
||||||
|
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
||||||
|
model_alias += "-latest"
|
||||||
|
model_options.append(
|
||||||
|
SelectOptionDict(
|
||||||
|
label=model_info.display_name,
|
||||||
|
value=model_alias,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return model_options
|
||||||
|
|
||||||
async def _get_location_data(self) -> dict[str, str]:
|
async def _get_location_data(self) -> dict[str, str]:
|
||||||
"""Get approximate location data of the user."""
|
"""Get approximate location data of the user."""
|
||||||
location_data: dict[str, str] = {}
|
location_data: dict[str, str] = {}
|
||||||
@@ -418,7 +455,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
response = await client.messages.create(
|
response = await client.messages.create(
|
||||||
model=RECOMMENDED_CHAT_MODEL,
|
model=cast(str, DEFAULT[CONF_CHAT_MODEL]),
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -433,7 +470,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
"content": "{", # hints the model to skip any preamble
|
"content": "{", # hints the model to skip any preamble
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_tokens=RECOMMENDED_MAX_TOKENS,
|
max_tokens=cast(int, DEFAULT[CONF_MAX_TOKENS]),
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Model response: %s", response.content)
|
_LOGGER.debug("Model response: %s", response.content)
|
||||||
location_data = location_schema(
|
location_data = location_schema(
|
||||||
|
|||||||
@@ -11,25 +11,29 @@ DEFAULT_AI_TASK_NAME = "Claude AI Task"
|
|||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_PROMPT = "prompt"
|
CONF_PROMPT = "prompt"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
RECOMMENDED_CHAT_MODEL = "claude-3-5-haiku-latest"
|
|
||||||
CONF_MAX_TOKENS = "max_tokens"
|
CONF_MAX_TOKENS = "max_tokens"
|
||||||
RECOMMENDED_MAX_TOKENS = 3000
|
|
||||||
CONF_TEMPERATURE = "temperature"
|
CONF_TEMPERATURE = "temperature"
|
||||||
RECOMMENDED_TEMPERATURE = 1.0
|
|
||||||
CONF_THINKING_BUDGET = "thinking_budget"
|
CONF_THINKING_BUDGET = "thinking_budget"
|
||||||
RECOMMENDED_THINKING_BUDGET = 0
|
|
||||||
MIN_THINKING_BUDGET = 1024
|
|
||||||
CONF_WEB_SEARCH = "web_search"
|
CONF_WEB_SEARCH = "web_search"
|
||||||
RECOMMENDED_WEB_SEARCH = False
|
|
||||||
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
|
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
|
||||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False
|
|
||||||
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
|
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
|
||||||
RECOMMENDED_WEB_SEARCH_MAX_USES = 5
|
|
||||||
CONF_WEB_SEARCH_CITY = "city"
|
CONF_WEB_SEARCH_CITY = "city"
|
||||||
CONF_WEB_SEARCH_REGION = "region"
|
CONF_WEB_SEARCH_REGION = "region"
|
||||||
CONF_WEB_SEARCH_COUNTRY = "country"
|
CONF_WEB_SEARCH_COUNTRY = "country"
|
||||||
CONF_WEB_SEARCH_TIMEZONE = "timezone"
|
CONF_WEB_SEARCH_TIMEZONE = "timezone"
|
||||||
|
|
||||||
|
DEFAULT = {
|
||||||
|
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
|
||||||
|
CONF_MAX_TOKENS: 3000,
|
||||||
|
CONF_TEMPERATURE: 1.0,
|
||||||
|
CONF_THINKING_BUDGET: 0,
|
||||||
|
CONF_WEB_SEARCH: False,
|
||||||
|
CONF_WEB_SEARCH_USER_LOCATION: False,
|
||||||
|
CONF_WEB_SEARCH_MAX_USES: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
MIN_THINKING_BUDGET = 1024
|
||||||
|
|
||||||
NON_THINKING_MODELS = [
|
NON_THINKING_MODELS = [
|
||||||
"claude-3-5", # Both sonnet and haiku
|
"claude-3-5", # Both sonnet and haiku
|
||||||
"claude-3-opus",
|
"claude-3-opus",
|
||||||
|
|||||||
@@ -84,14 +84,11 @@ from .const import (
|
|||||||
CONF_WEB_SEARCH_REGION,
|
CONF_WEB_SEARCH_REGION,
|
||||||
CONF_WEB_SEARCH_TIMEZONE,
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
CONF_WEB_SEARCH_USER_LOCATION,
|
CONF_WEB_SEARCH_USER_LOCATION,
|
||||||
|
DEFAULT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
MIN_THINKING_BUDGET,
|
MIN_THINKING_BUDGET,
|
||||||
NON_THINKING_MODELS,
|
NON_THINKING_MODELS,
|
||||||
RECOMMENDED_CHAT_MODEL,
|
|
||||||
RECOMMENDED_MAX_TOKENS,
|
|
||||||
RECOMMENDED_TEMPERATURE,
|
|
||||||
RECOMMENDED_THINKING_BUDGET,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Max number of back and forth with the LLM to generate a response
|
# Max number of back and forth with the LLM to generate a response
|
||||||
@@ -392,7 +389,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
type="tool_use",
|
type="tool_use",
|
||||||
id=response.content_block.id,
|
id=response.content_block.id,
|
||||||
name=response.content_block.name,
|
name=response.content_block.name,
|
||||||
input="",
|
input={},
|
||||||
)
|
)
|
||||||
current_tool_args = ""
|
current_tool_args = ""
|
||||||
if response.content_block.name == output_tool:
|
if response.content_block.name == output_tool:
|
||||||
@@ -459,7 +456,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
type="server_tool_use",
|
type="server_tool_use",
|
||||||
id=response.content_block.id,
|
id=response.content_block.id,
|
||||||
name=response.content_block.name,
|
name=response.content_block.name,
|
||||||
input="",
|
input={},
|
||||||
)
|
)
|
||||||
current_tool_args = ""
|
current_tool_args = ""
|
||||||
elif isinstance(response.content_block, WebSearchToolResultBlock):
|
elif isinstance(response.content_block, WebSearchToolResultBlock):
|
||||||
@@ -586,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -604,17 +601,19 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
raise TypeError("First message must be a system message")
|
raise TypeError("First message must be a system message")
|
||||||
messages = _convert_content(chat_log.content[1:])
|
messages = _convert_content(chat_log.content[1:])
|
||||||
|
|
||||||
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||||
|
|
||||||
model_args = MessageCreateParamsStreaming(
|
model_args = MessageCreateParamsStreaming(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||||
system=system.content,
|
system=system.content,
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
|
thinking_budget = options.get(
|
||||||
|
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not model.startswith(tuple(NON_THINKING_MODELS))
|
not model.startswith(tuple(NON_THINKING_MODELS))
|
||||||
and thinking_budget >= MIN_THINKING_BUDGET
|
and thinking_budget >= MIN_THINKING_BUDGET
|
||||||
@@ -625,7 +624,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
else:
|
else:
|
||||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||||
model_args["temperature"] = options.get(
|
model_args["temperature"] = options.get(
|
||||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
|
||||||
)
|
)
|
||||||
|
|
||||||
tools: list[ToolUnionParam] = []
|
tools: list[ToolUnionParam] = []
|
||||||
|
|||||||
@@ -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.69.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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user