mirror of
https://github.com/home-assistant/core.git
synced 2026-01-05 22:58:06 +00:00
Compare commits
980 Commits
trigger_de
...
entity_reg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5872d719 | ||
|
|
55b0406960 | ||
|
|
36483dd785 | ||
|
|
ad154dce40 | ||
|
|
3abf91af3a | ||
|
|
579d217c6b | ||
|
|
7322bee4dd | ||
|
|
8a36ec88f4 | ||
|
|
864f908257 | ||
|
|
5d86d8b380 | ||
|
|
15245707a5 | ||
|
|
80e4451a3f | ||
|
|
f051f4ea99 | ||
|
|
7717b5aca6 | ||
|
|
81d2bcdeb9 | ||
|
|
9934de18ae | ||
|
|
aac015e822 | ||
|
|
1f584f011e | ||
|
|
2106c4cfb9 | ||
|
|
a053142601 | ||
|
|
dd0dce7968 | ||
|
|
bdfff6df2d | ||
|
|
671c4e1eab | ||
|
|
8aae2a935a | ||
|
|
9e64f18439 | ||
|
|
e8a6f2f098 | ||
|
|
8faeb1fe98 | ||
|
|
edc48e0604 | ||
|
|
eab77f11b0 | ||
|
|
edb79b0337 | ||
|
|
41f33a106f | ||
|
|
cf31401cc2 | ||
|
|
8679c8e40c | ||
|
|
e675d0e8ed | ||
|
|
c73289aed9 | ||
|
|
4420776977 | ||
|
|
b77d6e7b59 | ||
|
|
8f074e5724 | ||
|
|
b1e46bcde4 | ||
|
|
fc4b5f66ff | ||
|
|
55978f2827 | ||
|
|
010a8cc693 | ||
|
|
d31eadc8cd | ||
|
|
3190a523aa | ||
|
|
8f82e451cd | ||
|
|
5bbd71e594 | ||
|
|
33257b8422 | ||
|
|
b3a4cd5b76 | ||
|
|
dcfa466dd4 | ||
|
|
846e6d96a4 | ||
|
|
d72b35a0cd | ||
|
|
5e003627b2 | ||
|
|
a4f71f37f6 | ||
|
|
c37b2f86b1 | ||
|
|
926aeef156 | ||
|
|
ee86671d39 | ||
|
|
dc371cf46d | ||
|
|
c76e26508d | ||
|
|
a5cd316fa3 | ||
|
|
736cc8a17d | ||
|
|
5278fce218 | ||
|
|
8f04f22c65 | ||
|
|
a01f638fc6 | ||
|
|
22005dd48a | ||
|
|
fff60b3863 | ||
|
|
5cb5fe5b67 | ||
|
|
24ea5eb9b5 | ||
|
|
673c2a77e0 | ||
|
|
ad3014e711 | ||
|
|
c19ae81cbc | ||
|
|
959d99f333 | ||
|
|
7cfe6bf427 | ||
|
|
765e2c1b6c | ||
|
|
0fd63df123 | ||
|
|
862fbd551a | ||
|
|
6e79b76d15 | ||
|
|
f85307d86c | ||
|
|
b01f93119f | ||
|
|
4130f3db2f | ||
|
|
ffcd5167b5 | ||
|
|
e94a7b2ec1 | ||
|
|
6b3f2e9b7b | ||
|
|
56545dacb0 | ||
|
|
cbf061183e | ||
|
|
5dcb5f4926 | ||
|
|
5fbb99a79a | ||
|
|
da65c52f2d | ||
|
|
08a850cfc7 | ||
|
|
12978092f7 | ||
|
|
210a9ad2de | ||
|
|
61328129fc | ||
|
|
f4673f44ee | ||
|
|
3bdd532dcd | ||
|
|
e23d3c8ab4 | ||
|
|
a7cb66c592 | ||
|
|
8544d1ebec | ||
|
|
240afd80c1 | ||
|
|
ccb1da3a97 | ||
|
|
de62991e5b | ||
|
|
bad75222ed | ||
|
|
8fc334b338 | ||
|
|
abb59f2233 | ||
|
|
f583dfe532 | ||
|
|
cd40b7eed6 | ||
|
|
dad96598a3 | ||
|
|
669527b1e9 | ||
|
|
090c74f18e | ||
|
|
4b9594b876 | ||
|
|
cd5bfd6baf | ||
|
|
2ef335f403 | ||
|
|
22e70723f4 | ||
|
|
aac572c457 | ||
|
|
8f9167abbe | ||
|
|
d0deb16c10 | ||
|
|
ad37e00d1d | ||
|
|
4821c9ec29 | ||
|
|
81a5b4a684 | ||
|
|
20e4d37cc6 | ||
|
|
adfdeff84c | ||
|
|
0d29b2d5a7 | ||
|
|
e894a03c43 | ||
|
|
43a1a679f9 | ||
|
|
1759bfbfaf | ||
|
|
fc1c0d22b9 | ||
|
|
3a48c9569c | ||
|
|
85f3f180ab | ||
|
|
bfd4f85225 | ||
|
|
2abb914867 | ||
|
|
8b10128c50 | ||
|
|
10bf1cb999 | ||
|
|
6e45713d3a | ||
|
|
50a2eba66e | ||
|
|
0bb16befbd | ||
|
|
d72cc45ca8 | ||
|
|
d4bc066cc4 | ||
|
|
c2c561bc21 | ||
|
|
5a6e26fedf | ||
|
|
51c7bafb41 | ||
|
|
c8964494a2 | ||
|
|
5db5f36554 | ||
|
|
11f1e376c4 | ||
|
|
0139407f52 | ||
|
|
d278d21561 | ||
|
|
efce6c8468 | ||
|
|
8c7e9bcf7c | ||
|
|
8bcc9485c3 | ||
|
|
bc8e00d9e0 | ||
|
|
d5c208672e | ||
|
|
376f6ce4a7 | ||
|
|
370bb14b46 | ||
|
|
3d1773fca5 | ||
|
|
977d4c8f01 | ||
|
|
bf0bcc4c95 | ||
|
|
ecb51ce185 | ||
|
|
87f0703be1 | ||
|
|
a90ac612f0 | ||
|
|
e2faa7020b | ||
|
|
ce523fc91d | ||
|
|
5bb96f7f06 | ||
|
|
dfbe42fb21 | ||
|
|
c9876e2a2b | ||
|
|
4ee9eada41 | ||
|
|
e82d91d394 | ||
|
|
60e91555f8 | ||
|
|
e5f163fa56 | ||
|
|
060f748287 | ||
|
|
0031bce832 | ||
|
|
4c166c2320 | ||
|
|
340a5f92e5 | ||
|
|
32645bead0 | ||
|
|
04f00d7010 | ||
|
|
df9b0432b9 | ||
|
|
47dbf923ed | ||
|
|
d0c5f291fc | ||
|
|
4b5ab472ad | ||
|
|
ee9abd519d | ||
|
|
1d2599184b | ||
|
|
b67d34d428 | ||
|
|
b203a04f1b | ||
|
|
50108e23ed | ||
|
|
58339d79d3 | ||
|
|
05c8e8b4fd | ||
|
|
ae676d6857 | ||
|
|
db4d51e617 | ||
|
|
bfe84ccd12 | ||
|
|
ede948c277 | ||
|
|
28e8405622 | ||
|
|
202d285286 | ||
|
|
c0b1536cd8 | ||
|
|
f1d2b102cf | ||
|
|
8b29e3011e | ||
|
|
dea5e7454a | ||
|
|
58d4fd0b75 | ||
|
|
ca37bc1506 | ||
|
|
1f25641e3b | ||
|
|
962b276a77 | ||
|
|
0786e333ce | ||
|
|
d35271a214 | ||
|
|
8fbf6dbd0b | ||
|
|
60d8353b10 | ||
|
|
2d69d2ef1c | ||
|
|
e1e11db3d2 | ||
|
|
91ef5fb429 | ||
|
|
0dc2a5a02c | ||
|
|
b018465a4d | ||
|
|
e797d651b5 | ||
|
|
bc6f261105 | ||
|
|
6c2ba15a73 | ||
|
|
ef0712a785 | ||
|
|
2bd45e4625 | ||
|
|
03ca164fb3 | ||
|
|
3c11f8e50e | ||
|
|
f0e5325510 | ||
|
|
4ff2da7553 | ||
|
|
af951ff0d4 | ||
|
|
207e2f61ea | ||
|
|
9e204cd347 | ||
|
|
97a7e46579 | ||
|
|
8175f4ba65 | ||
|
|
22e307a48a | ||
|
|
6c69b36b7b | ||
|
|
dd270f54fc | ||
|
|
158fb35c5b | ||
|
|
85e5081fc3 | ||
|
|
f6bb32c44b | ||
|
|
e16eff6f28 | ||
|
|
2b80a621af | ||
|
|
80d2e08572 | ||
|
|
e15166cbf3 | ||
|
|
3b1b095b47 | ||
|
|
72e78b6719 | ||
|
|
a50b035479 | ||
|
|
bb4f8adffe | ||
|
|
61a50e77cf | ||
|
|
82f94de0b8 | ||
|
|
71b2d46afd | ||
|
|
edc1989ff6 | ||
|
|
2dad6fa298 | ||
|
|
2f4e29ba71 | ||
|
|
add75e06e3 | ||
|
|
9194ddd4fe | ||
|
|
2e7821d64a | ||
|
|
bb9660269c | ||
|
|
1bd5aa0ab0 | ||
|
|
e4329ab8a5 | ||
|
|
9414356a4d | ||
|
|
0cd28e7fc1 | ||
|
|
6383f9365c | ||
|
|
cb8669c84f | ||
|
|
d1698222f4 | ||
|
|
a3f5c3f422 | ||
|
|
945771098e | ||
|
|
59d73138e7 | ||
|
|
fe71b54c3e | ||
|
|
7639e12ff2 | ||
|
|
4b2a149072 | ||
|
|
92b988a292 | ||
|
|
1ca6c4b5b8 | ||
|
|
81377be92f | ||
|
|
38aba81f62 | ||
|
|
e175e3ed0b | ||
|
|
27b32c5e93 | ||
|
|
332996cc38 | ||
|
|
199b7e8ba7 | ||
|
|
122af46a92 | ||
|
|
3dd091de44 | ||
|
|
932c5ccf0f | ||
|
|
4e52826664 | ||
|
|
c30d778a54 | ||
|
|
7cd767e920 | ||
|
|
4c39936b81 | ||
|
|
d90590b228 | ||
|
|
7914e6b135 | ||
|
|
1d646d06a6 | ||
|
|
135c80d194 | ||
|
|
aed853267a | ||
|
|
62be31f899 | ||
|
|
ca75581d43 | ||
|
|
f8a8fb516b | ||
|
|
f9575a3b2f | ||
|
|
bbde98bc9f | ||
|
|
90e8d74fcd | ||
|
|
9489c19598 | ||
|
|
210ec28f0a | ||
|
|
88d853cfbd | ||
|
|
069b21a5a5 | ||
|
|
da864ca034 | ||
|
|
8d6b7bf950 | ||
|
|
cf8e7cfd28 | ||
|
|
8e5f9264b6 | ||
|
|
3d0ecf0585 | ||
|
|
19b2c6da23 | ||
|
|
0b231ff042 | ||
|
|
d8ae89be6a | ||
|
|
e96ff77cbf | ||
|
|
9797d391af | ||
|
|
e68df66028 | ||
|
|
8d30d69af5 | ||
|
|
93e9fab6c7 | ||
|
|
48300f4563 | ||
|
|
48091e5995 | ||
|
|
26582cecbd | ||
|
|
7ecf32390c | ||
|
|
cded163930 | ||
|
|
10fe479311 | ||
|
|
65696f9b53 | ||
|
|
08fc2ab03b | ||
|
|
2290940638 | ||
|
|
63640af4d4 | ||
|
|
f4400516b8 | ||
|
|
b52a806b36 | ||
|
|
76f3397aa0 | ||
|
|
89abe65e1d | ||
|
|
785c9ebc3b | ||
|
|
a08be4fcb6 | ||
|
|
e8409e7c42 | ||
|
|
c46618cbd1 | ||
|
|
319e37384f | ||
|
|
4c1788e757 | ||
|
|
e6a158b1ac | ||
|
|
69dbcb0627 | ||
|
|
c62c52e8cf | ||
|
|
c18dc9b63b | ||
|
|
69757bed52 | ||
|
|
899f0e03c1 | ||
|
|
fc5e720764 | ||
|
|
0c0e54b541 | ||
|
|
86e3eca57f | ||
|
|
15505cdd56 | ||
|
|
c7001dcfc4 | ||
|
|
40feefc0fa | ||
|
|
f6d23b9b34 | ||
|
|
ab4aeb65f2 | ||
|
|
019c4ab874 | ||
|
|
53ca369395 | ||
|
|
9910480980 | ||
|
|
7ecbe53b15 | ||
|
|
6baa162963 | ||
|
|
2f5561aeba | ||
|
|
330bb46cf9 | ||
|
|
2f8ddae24d | ||
|
|
419315d9cf | ||
|
|
9138930cb9 | ||
|
|
a325596898 | ||
|
|
5fdb95e83c | ||
|
|
fcbfca52f3 | ||
|
|
f44578f45f | ||
|
|
2b7bd923d6 | ||
|
|
3ab4fd3035 | ||
|
|
794deaa5fd | ||
|
|
79bbae2fde | ||
|
|
9f17a8a943 | ||
|
|
b44c47cd80 | ||
|
|
e80c090932 | ||
|
|
ff418f513a | ||
|
|
b222cc5889 | ||
|
|
db1707fd72 | ||
|
|
6f6f5809d0 | ||
|
|
1f43f82ea6 | ||
|
|
942274234e | ||
|
|
f03955b773 | ||
|
|
27ac375183 | ||
|
|
c951728767 | ||
|
|
3496494290 | ||
|
|
e90183391e | ||
|
|
90558c517b | ||
|
|
7fba94747e | ||
|
|
3b4b478afa | ||
|
|
a3640c5664 | ||
|
|
246a181ad4 | ||
|
|
d642ecb302 | ||
|
|
53889165b5 | ||
|
|
fe32e74910 | ||
|
|
a71ae4db37 | ||
|
|
0d5ebdb692 | ||
|
|
80e720f663 | ||
|
|
616b031df8 | ||
|
|
bcdece4455 | ||
|
|
1aa3efaf8a | ||
|
|
7f16b11776 | ||
|
|
078b7224fc | ||
|
|
d7320f00ea | ||
|
|
0bcc0f3fb9 | ||
|
|
c551a133c1 | ||
|
|
22e19e768e | ||
|
|
0647222402 | ||
|
|
83226ed015 | ||
|
|
837472c12d | ||
|
|
0b337c7e2a | ||
|
|
1a0b61c98e | ||
|
|
87a2d3e6d9 | ||
|
|
2c1407f159 | ||
|
|
1643d5df67 | ||
|
|
5a49007b86 | ||
|
|
312d8aaff5 | ||
|
|
776726a053 | ||
|
|
4213427b9c | ||
|
|
82907e5b88 | ||
|
|
56b4c554de | ||
|
|
82390f6f7b | ||
|
|
d9ebda4910 | ||
|
|
8f94657b0c | ||
|
|
b0ab3cddb8 | ||
|
|
3d4d57fa32 | ||
|
|
fed6f19edf | ||
|
|
2725abf032 | ||
|
|
bd1b81493c | ||
|
|
c58a188179 | ||
|
|
ffbb7a2ab4 | ||
|
|
2ad470d172 | ||
|
|
8b2fce9c33 | ||
|
|
e22e7f1bcf | ||
|
|
b5bd61b20a | ||
|
|
391c9a679e | ||
|
|
d5970e7733 | ||
|
|
d5a74892e6 | ||
|
|
793a829236 | ||
|
|
7670146faf | ||
|
|
eaedefe105 | ||
|
|
4f20776e0e | ||
|
|
6c21a14be4 | ||
|
|
9015743483 | ||
|
|
2a62e033dd | ||
|
|
f72f2a326a | ||
|
|
61de50dfc0 | ||
|
|
ef7ed026db | ||
|
|
abdb48e7ce | ||
|
|
9646aa232a | ||
|
|
635cfe7d17 | ||
|
|
1e2f7cadc7 | ||
|
|
94e9f32da5 | ||
|
|
b7ba99ed17 | ||
|
|
ebbeef8021 | ||
|
|
8da75490c0 | ||
|
|
bc89e8fd3c | ||
|
|
602497904b | ||
|
|
facf217b99 | ||
|
|
b300654e15 | ||
|
|
a742125f13 | ||
|
|
64768b1036 | ||
|
|
8d49cb1195 | ||
|
|
792bb5781d | ||
|
|
7bd126dc8e | ||
|
|
83ee380b17 | ||
|
|
58f8b3c401 | ||
|
|
2a6d1180f4 | ||
|
|
00b765893d | ||
|
|
3e9e9b0489 | ||
|
|
25f7c02498 | ||
|
|
a785f3d509 | ||
|
|
9f36b2dcde | ||
|
|
57265ac648 | ||
|
|
f5fe53a67f | ||
|
|
7e6ceee9d1 | ||
|
|
9c21965a34 | ||
|
|
1ea740d81c | ||
|
|
6e98446523 | ||
|
|
2248584a0f | ||
|
|
d9b6f82639 | ||
|
|
3eecfa8e57 | ||
|
|
382e7dfd39 | ||
|
|
5358c89bfd | ||
|
|
e6103fdcf4 | ||
|
|
02dca5f0ad | ||
|
|
cc4b9e0eca | ||
|
|
7e28e3dcd3 | ||
|
|
bb3d571887 | ||
|
|
5a789cbbc8 | ||
|
|
4954c2a84b | ||
|
|
f28e9f60ee | ||
|
|
6a4bf4ec72 | ||
|
|
12706178c2 | ||
|
|
ed39b18d94 | ||
|
|
9999807891 | ||
|
|
b5db0e98b4 | ||
|
|
f58b2177a2 | ||
|
|
4f64014816 | ||
|
|
cf68214c4d | ||
|
|
b3d3284f5c | ||
|
|
12c346f550 | ||
|
|
bda82e19a5 | ||
|
|
f7726a7563 | ||
|
|
2c0ed2cbfe | ||
|
|
13376ef896 | ||
|
|
d18cc3d6c3 | ||
|
|
b40aab479a | ||
|
|
721f9a40d8 | ||
|
|
eb4b75a9a7 | ||
|
|
b40f381164 | ||
|
|
51413b7a8d | ||
|
|
ff694a0058 | ||
|
|
eea04558a9 | ||
|
|
5ad2a27918 | ||
|
|
f39305f64e | ||
|
|
7fba0ca2c0 | ||
|
|
51fbccd125 | ||
|
|
5fc2e6ed53 | ||
|
|
5a7f7d90a0 | ||
|
|
6d34d34ce1 | ||
|
|
6454f40c3c | ||
|
|
53e40a6b8c | ||
|
|
8a54a1d95c | ||
|
|
8a52e9ca01 | ||
|
|
d9ca253c6c | ||
|
|
b7853ea9bd | ||
|
|
d19e410ea8 | ||
|
|
83f911e4ff | ||
|
|
452322e971 | ||
|
|
6fa7c6cb81 | ||
|
|
ed6072d46b | ||
|
|
9fdc632780 | ||
|
|
4d426c31f9 | ||
|
|
ea946c90b3 | ||
|
|
fb68b2d454 | ||
|
|
2ebe0a929e | ||
|
|
c1e5a7efc9 | ||
|
|
561ef7015c | ||
|
|
b4270e019e | ||
|
|
614bf96fb9 | ||
|
|
ca290ee631 | ||
|
|
ad3174f6e6 | ||
|
|
218b0738ca | ||
|
|
98e6e20079 | ||
|
|
89aa349881 | ||
|
|
07930b12d0 | ||
|
|
711afa306c | ||
|
|
a3904ce60c | ||
|
|
455cf2fb42 | ||
|
|
072ae2b955 | ||
|
|
2b70639b11 | ||
|
|
2612dbeb9b | ||
|
|
7ebdd24224 | ||
|
|
66ff1cf005 | ||
|
|
08aae4bf49 | ||
|
|
313b5a483c | ||
|
|
8edbcc92d3 | ||
|
|
067cab71fa | ||
|
|
596e4883b1 | ||
|
|
fb4a452872 | ||
|
|
5b232226e9 | ||
|
|
db81610983 | ||
|
|
8f5c8caf07 | ||
|
|
f6af524ddf | ||
|
|
e0a8c9b458 | ||
|
|
c46412ee5b | ||
|
|
a06df2a680 | ||
|
|
68fbcc8665 | ||
|
|
6cde5cfdcc | ||
|
|
5605f5896a | ||
|
|
93c30f1b59 | ||
|
|
6e3ccbefc2 | ||
|
|
715dc12792 | ||
|
|
9cae0e0acc | ||
|
|
e13702d9b1 | ||
|
|
3b358df9e7 | ||
|
|
e394435d7c | ||
|
|
9e398ffc10 | ||
|
|
065a53a90d | ||
|
|
dc8aaac6fb | ||
|
|
1e87f0cab1 | ||
|
|
1aeced0fe6 | ||
|
|
91f6b8e1fe | ||
|
|
1a9d1a9649 | ||
|
|
cb7c7767b5 | ||
|
|
d02029143c | ||
|
|
3eda687d30 | ||
|
|
7688c367cc | ||
|
|
a1dc3f3eac | ||
|
|
7ed14f0afd | ||
|
|
dc5d159ffb | ||
|
|
5fdd04b860 | ||
|
|
6f5d72fd81 | ||
|
|
d135d08813 | ||
|
|
9595759fd1 | ||
|
|
d54f979612 | ||
|
|
531073acc0 | ||
|
|
73cbc962f9 | ||
|
|
34b0b71375 | ||
|
|
3158aa8891 | ||
|
|
0c74e22069 | ||
|
|
a88549315c | ||
|
|
fde548b825 | ||
|
|
91b10fb6d7 | ||
|
|
203c908730 | ||
|
|
39f41fe17d | ||
|
|
3d39fb08e5 | ||
|
|
a1731cd210 | ||
|
|
0c31ec9bb6 | ||
|
|
23e6148d3b | ||
|
|
2a5a66f9d5 | ||
|
|
84ce5d65e1 | ||
|
|
762c179b80 | ||
|
|
66019953db | ||
|
|
a2931efeeb | ||
|
|
90c03f4115 | ||
|
|
8afe3fed74 | ||
|
|
3ef332e168 | ||
|
|
8d821d9f98 | ||
|
|
23619fb2d3 | ||
|
|
bc70aeea85 | ||
|
|
beca01e857 | ||
|
|
4765d9da92 | ||
|
|
7951e822be | ||
|
|
c653bfff9f | ||
|
|
2223bdb48e | ||
|
|
42a3bef34a | ||
|
|
6f4d405b26 | ||
|
|
8edc5f0359 | ||
|
|
efcffd1016 | ||
|
|
ee32992010 | ||
|
|
319128043e | ||
|
|
00c7838587 | ||
|
|
d8b576c087 | ||
|
|
330dce24c5 | ||
|
|
0089d3efa1 | ||
|
|
167e9c8f4a | ||
|
|
c7f5e25d41 | ||
|
|
7b5dd4a0ec | ||
|
|
84de6aacfc | ||
|
|
9561c84920 | ||
|
|
7572b2a669 | ||
|
|
b48409ab1b | ||
|
|
ab04e2c501 | ||
|
|
38e6a7c6d4 | ||
|
|
c2b284de2d | ||
|
|
b760bf342a | ||
|
|
79cfea3fea | ||
|
|
69ace08c01 | ||
|
|
bf33e286d6 | ||
|
|
6b83effc5f | ||
|
|
2b158fe690 | ||
|
|
712ddc03c8 | ||
|
|
efe519faad | ||
|
|
1b7cb418eb | ||
|
|
c678bcd4f1 | ||
|
|
0eaea13e8d | ||
|
|
b1e4513f7d | ||
|
|
6d7f8bb7d7 | ||
|
|
b481aaba77 | ||
|
|
d539f37aa4 | ||
|
|
865b3a6646 | ||
|
|
1c603f968f | ||
|
|
d821d27730 | ||
|
|
dfa060a7e1 | ||
|
|
5262cca8e6 | ||
|
|
2c36a74da5 | ||
|
|
084cde6ecf | ||
|
|
3e34aa5fb7 | ||
|
|
268f0d9e03 | ||
|
|
f8d3bc1b89 | ||
|
|
fb64ff1d17 | ||
|
|
ff72faf83a | ||
|
|
acb58c41eb | ||
|
|
586b197fc3 | ||
|
|
5c1d16d582 | ||
|
|
73be4625ae | ||
|
|
775701133d | ||
|
|
1af0282091 | ||
|
|
c876bed33f | ||
|
|
e9d39a826e | ||
|
|
f9e1c07c04 | ||
|
|
c0bef51563 | ||
|
|
b41a9575af | ||
|
|
e585b3abd1 | ||
|
|
5d2877f454 | ||
|
|
2d89c60ac5 | ||
|
|
860a7b7d91 | ||
|
|
5585376b40 | ||
|
|
c4cb70fc06 | ||
|
|
981ae39182 | ||
|
|
dff4f79925 | ||
|
|
bf64e11960 | ||
|
|
823d20c67f | ||
|
|
1a654cd35d | ||
|
|
13e592edaf | ||
|
|
94191239c6 | ||
|
|
91a1ca09f7 | ||
|
|
9f1fe8a067 | ||
|
|
f2c9cdb09e | ||
|
|
712115cdb8 | ||
|
|
eb6ae9d2d6 | ||
|
|
b126f3fa66 | ||
|
|
2d720f0d32 | ||
|
|
c0155f5e80 | ||
|
|
23a2d69984 | ||
|
|
a8779d5f52 | ||
|
|
01c197e830 | ||
|
|
ef4f476844 | ||
|
|
8aee05b8b0 | ||
|
|
0f3f8d5707 | ||
|
|
2948b1c58e | ||
|
|
4cb2af4d08 | ||
|
|
8e12d2028d | ||
|
|
5b046def8e | ||
|
|
6a81bf6f5e | ||
|
|
102d6a37c0 | ||
|
|
fd6aba3022 | ||
|
|
a88eadf863 | ||
|
|
52f0d04c38 | ||
|
|
3ab80c6ff2 | ||
|
|
71485871c8 | ||
|
|
ba0da4c2a3 | ||
|
|
cbaadebac3 | ||
|
|
fd0ae32058 | ||
|
|
382bf78ee0 | ||
|
|
6aa077a48d | ||
|
|
b638fcbaad | ||
|
|
704edac9fd | ||
|
|
ff9e2a8f1e | ||
|
|
d778afe61a | ||
|
|
448084e2b5 | ||
|
|
d99379ffdf | ||
|
|
b835b7f266 | ||
|
|
e96e97edca | ||
|
|
df7c657d7e | ||
|
|
4f5502ab47 | ||
|
|
c30ee776e9 | ||
|
|
efebdc0181 | ||
|
|
da7fc88f1f | ||
|
|
566aeb5e9a | ||
|
|
d17f0ef55a | ||
|
|
35025c4b59 | ||
|
|
e5d512d5e5 | ||
|
|
2b5028bfb7 | ||
|
|
f074d81c8b | ||
|
|
d791d66104 | ||
|
|
757fee9f73 | ||
|
|
06130219b4 | ||
|
|
4e2fe63182 | ||
|
|
dd9bd50a7b | ||
|
|
d0cc9990dd | ||
|
|
6243517271 | ||
|
|
76ca9ce3a4 | ||
|
|
124e7cf4c8 | ||
|
|
ad8ff7570d | ||
|
|
c4c14bee36 | ||
|
|
2cf5badc17 | ||
|
|
0478f43b4b | ||
|
|
d18f6273a8 | ||
|
|
94bade0202 | ||
|
|
260ea9a3be | ||
|
|
e1f6820cb6 | ||
|
|
2215777cfb | ||
|
|
fa3ce62ae8 | ||
|
|
33421bddf3 | ||
|
|
1efe2b437d | ||
|
|
a54f0adf74 | ||
|
|
afe574f74e | ||
|
|
25aae8944d | ||
|
|
f26e6ad211 | ||
|
|
855e8b08e9 | ||
|
|
9820956b46 | ||
|
|
1693299652 | ||
|
|
75200a9426 | ||
|
|
fa587cec38 | ||
|
|
47946d0103 | ||
|
|
e9444a2e4d | ||
|
|
60988534a9 | ||
|
|
d2586ca4ff | ||
|
|
932bf81ac8 | ||
|
|
4e21ef5fbc | ||
|
|
a9998b41a5 | ||
|
|
0a72f31504 | ||
|
|
f3a50c176d | ||
|
|
b6b422775a | ||
|
|
00baecd01e | ||
|
|
b370b7a7f6 | ||
|
|
baa2d751e4 | ||
|
|
c8d54fcffc | ||
|
|
80e3655bac | ||
|
|
e5b0a366fe | ||
|
|
20e78a15b4 | ||
|
|
9d806aef88 | ||
|
|
7e16973166 | ||
|
|
e5f776fdc3 | ||
|
|
83ccdb35f1 | ||
|
|
52984f2fd1 | ||
|
|
a548e13da5 | ||
|
|
55301a50b2 | ||
|
|
1302b6744e | ||
|
|
0aeff366bd | ||
|
|
0db23b0da6 | ||
|
|
863e2074b6 | ||
|
|
13828f6713 | ||
|
|
fdb38ec8ec | ||
|
|
55abb6e594 | ||
|
|
a83e4f5c63 | ||
|
|
cba15ee439 | ||
|
|
400620399a | ||
|
|
28e19215ad | ||
|
|
119d0a0170 | ||
|
|
69faf38e86 | ||
|
|
d0ef1a1a8b | ||
|
|
8f328810bf | ||
|
|
4f1b75e3b4 | ||
|
|
445a7fc749 | ||
|
|
977c0797aa | ||
|
|
a24f027923 | ||
|
|
7b45798e30 | ||
|
|
2b0cda0ad1 | ||
|
|
12dca4b1bf | ||
|
|
8c509b11b2 | ||
|
|
991c9008bd | ||
|
|
fe95f6e1c5 | ||
|
|
37510aa316 | ||
|
|
4e40e9bf74 | ||
|
|
70c9b1f095 | ||
|
|
f714388130 | ||
|
|
ffb2a693f4 | ||
|
|
9d8e253ad3 | ||
|
|
31631cc882 | ||
|
|
808273962d | ||
|
|
094fe43557 | ||
|
|
8f5bd51eef | ||
|
|
faf0ded854 | ||
|
|
d20302f97b | ||
|
|
74c25496bc | ||
|
|
67ecea0778 | ||
|
|
164e5871cb | ||
|
|
7a9966120e | ||
|
|
d810b4ca38 | ||
|
|
896062d669 | ||
|
|
03bd133577 | ||
|
|
4596c1644b | ||
|
|
778fe96eb6 | ||
|
|
a06557ed54 | ||
|
|
641621d184 | ||
|
|
b163f2b855 | ||
|
|
0c0604e5bd | ||
|
|
e0e4fc8afb | ||
|
|
f832a2844f | ||
|
|
4b0b268227 | ||
|
|
dfc16d9f15 | ||
|
|
4e3309bd22 | ||
|
|
a5a45ce59f | ||
|
|
6cb48da2f3 | ||
|
|
ab5aac47b2 | ||
|
|
d50b9405f0 | ||
|
|
a2722f08c4 | ||
|
|
aa700c3982 | ||
|
|
3b1bb41129 | ||
|
|
79ef51fb07 | ||
|
|
53769da55e | ||
|
|
82d153a240 | ||
|
|
0dac635478 | ||
|
|
90fc7d314b | ||
|
|
636c1b7e4f | ||
|
|
49c23de2d2 | ||
|
|
e48820b2c1 | ||
|
|
3a64357201 | ||
|
|
20fdec9e9c | ||
|
|
064a63fe1f | ||
|
|
803654223a | ||
|
|
a6148b50cf | ||
|
|
02a3c5be14 | ||
|
|
08ea640629 | ||
|
|
7dd761c9c3 | ||
|
|
6b827dfc33 | ||
|
|
67c19087dd | ||
|
|
55c7c2f730 | ||
|
|
afee936c3d | ||
|
|
ed2ced6c36 | ||
|
|
4c5cf028d7 | ||
|
|
68faa897ad | ||
|
|
53c9c42148 | ||
|
|
d48cc03be7 | ||
|
|
28236aa023 | ||
|
|
bfae07135a | ||
|
|
99d580e371 | ||
|
|
4d53450cbf | ||
|
|
1fbce01e26 | ||
|
|
a9621ac811 | ||
|
|
94f2118b19 | ||
|
|
73ca6b4900 | ||
|
|
31e647b5b0 | ||
|
|
fac5b2c09c | ||
|
|
ae48179e95 | ||
|
|
88c9d5dbe3 | ||
|
|
b76f47cd9f | ||
|
|
822e1ffc8d | ||
|
|
1632e0aef6 | ||
|
|
e2bc73f153 | ||
|
|
46cfdddc80 | ||
|
|
0bdf6757c4 | ||
|
|
312e590360 | ||
|
|
7a6aaf667b | ||
|
|
33eaca24d6 | ||
|
|
3d27d501b1 | ||
|
|
39b651e075 | ||
|
|
a962777a2e | ||
|
|
594ce8f266 | ||
|
|
9f867f268c | ||
|
|
9edd242734 | ||
|
|
93e11aa8bc | ||
|
|
c2b298283e | ||
|
|
106c086e8b | ||
|
|
cbf4130bff | ||
|
|
afffe0b08b | ||
|
|
c1ccfee7cc | ||
|
|
8d8383e1c1 | ||
|
|
f350a1a1fa | ||
|
|
fe2bd8d09e | ||
|
|
cf14226b02 | ||
|
|
bd3fe1d4ad | ||
|
|
377ca04be8 | ||
|
|
5837f55205 | ||
|
|
0766edb9c4 | ||
|
|
e62e3778f3 | ||
|
|
aa8e4c1c15 | ||
|
|
46ed8a73fc | ||
|
|
83f22497ae | ||
|
|
3dda1685dc | ||
|
|
6fa9d42401 | ||
|
|
2b7a434677 | ||
|
|
9ef7c6c99a | ||
|
|
b789c11217 | ||
|
|
5e8cd19cc3 | ||
|
|
027052440d | ||
|
|
47a7ed4084 | ||
|
|
89f6cfeb81 | ||
|
|
c268e57ba7 | ||
|
|
138c19126b | ||
|
|
c459ceba73 | ||
|
|
8d0ceff652 | ||
|
|
1d383e80a4 | ||
|
|
6a17a12be5 | ||
|
|
3a8d962d34 | ||
|
|
7e5cf17cf4 | ||
|
|
214940d04f | ||
|
|
6877fdaf5b | ||
|
|
35d0c254a2 | ||
|
|
9649fbc189 | ||
|
|
b60b1fc0c6 | ||
|
|
6b93f6d75c | ||
|
|
c8069a383e | ||
|
|
6857e87b30 | ||
|
|
a095631f4f | ||
|
|
c59fbdeec1 | ||
|
|
b521b1e64c | ||
|
|
073589ae19 | ||
|
|
9435b0ad3a | ||
|
|
1a54d566f8 | ||
|
|
1a9cae0f89 | ||
|
|
1662d36125 | ||
|
|
70e54fdadd | ||
|
|
38d0ebb8ba | ||
|
|
551dcaa169 | ||
|
|
5467db065b | ||
|
|
6a8d752e56 | ||
|
|
179a56628d | ||
|
|
b3f830773a | ||
|
|
084e06ec7d | ||
|
|
e0190afd3c | ||
|
|
b9e16d54c4 | ||
|
|
627785edc1 | ||
|
|
15cb48badb | ||
|
|
22214e8d31 | ||
|
|
fc04e0b2cc | ||
|
|
3fc6ebdb43 | ||
|
|
3ccb7deb3c | ||
|
|
f5f63b914a | ||
|
|
bd0a3f5a5d | ||
|
|
ab9eebd092 | ||
|
|
68c43099d9 | ||
|
|
041c417164 | ||
|
|
537d09c697 | ||
|
|
21e3b8da92 | ||
|
|
d390681360 | ||
|
|
918ec78348 | ||
|
|
1deae3ee1a | ||
|
|
59eace67df | ||
|
|
7eb7c66e3f | ||
|
|
aa2941592d | ||
|
|
29daf136d2 | ||
|
|
3da3cf7f52 | ||
|
|
d8c93d54d5 | ||
|
|
0799ee9fba | ||
|
|
9d31403984 | ||
|
|
02f87cba9b | ||
|
|
5b54784378 |
@@ -14,7 +14,8 @@ tests
|
||||
|
||||
# Other virtualization methods
|
||||
venv
|
||||
.venv
|
||||
.vagrant
|
||||
|
||||
# Temporary files
|
||||
**/__pycache__
|
||||
**/__pycache__
|
||||
|
||||
19
.github/copilot-instructions.md
vendored
19
.github/copilot-instructions.md
vendored
@@ -1073,7 +1073,11 @@ async def test_flow_connection_error(hass, mock_api_error):
|
||||
|
||||
### Entity Testing Patterns
|
||||
```python
|
||||
@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True)
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Overridden fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR] # Or another specific platform as needed.
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
@@ -1120,16 +1124,25 @@ def mock_device_api() -> Generator[MagicMock]:
|
||||
)
|
||||
yield api
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return PLATFORMS
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_device_api: MagicMock,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
```
|
||||
|
||||
|
||||
30
.github/workflows/builder.yml
vendored
30
.github/workflows/builder.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.9.2
|
||||
@@ -330,14 +330,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -462,7 +462,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -499,10 +499,10 @@ jobs:
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
104
.github/workflows/ci.yaml
vendored
104
.github/workflows/ci.yaml
vendored
@@ -37,10 +37,10 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 7
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.9"
|
||||
HA_SHORT_VERSION: "2025.10"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate_python_cache_key
|
||||
run: |
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
@@ -301,7 +301,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
@@ -341,7 +341,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
@@ -381,7 +381,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -462,7 +462,7 @@ jobs:
|
||||
- script/hassfest/docker/Dockerfile
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -481,7 +481,7 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -584,7 +584,7 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -593,7 +593,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -617,7 +617,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -626,7 +626,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -651,9 +651,9 @@ jobs:
|
||||
&& github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.7.1
|
||||
uses: actions/dependency-review-action@v4.7.3
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -674,7 +674,7 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -683,7 +683,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -717,7 +717,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -726,7 +726,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -764,7 +764,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -773,7 +773,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -809,7 +809,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -825,7 +825,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -833,7 +833,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -886,7 +886,7 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -895,7 +895,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -947,7 +947,7 @@ jobs:
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -956,7 +956,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -970,7 +970,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1080,7 +1080,7 @@ jobs:
|
||||
libmariadb-dev-compat \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -1089,7 +1089,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1222,7 +1222,7 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
postgresql-server-dev-14
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -1231,7 +1231,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1334,14 +1334,14 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.5.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1381,7 +1381,7 @@ jobs:
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -1390,7 +1390,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1484,14 +1484,14 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.5.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -1511,7 +1511,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.29.5
|
||||
uses: github/codeql-action/init@v3.29.11
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.5
|
||||
uses: github/codeql-action/analyze@v3.29.11
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v2.0.1
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@v1.2.3
|
||||
uses: actions/ai-inference@v2.0.1
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
2
.github/workflows/restrict-task-creation.yml
vendored
2
.github/workflows/restrict-task-creation.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.6.0
|
||||
|
||||
20
.github/workflows/wheels.yml
vendored
20
.github/workflows/wheels.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
@@ -135,20 +135,20 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -184,25 +184,25 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ repos:
|
||||
exclude_types: [csv, json, html]
|
||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
stages: [manual]
|
||||
|
||||
@@ -310,7 +310,6 @@ homeassistant.components.letpot.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
homeassistant.components.linear_garage_door.*
|
||||
homeassistant.components.linkplay.*
|
||||
homeassistant.components.litejet.*
|
||||
homeassistant.components.litterrobot.*
|
||||
@@ -467,6 +466,7 @@ homeassistant.components.simplisafe.*
|
||||
homeassistant.components.siren.*
|
||||
homeassistant.components.skybell.*
|
||||
homeassistant.components.slack.*
|
||||
homeassistant.components.sleep_as_android.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.smlight.*
|
||||
|
||||
26
CODEOWNERS
generated
26
CODEOWNERS
generated
@@ -87,6 +87,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
||||
/tests/components/aladdin_connect/ @swcloudgenie
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||
@@ -156,8 +158,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
||||
/tests/components/asuswrt/ @kennedyshead @ollo69
|
||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||
/homeassistant/components/atag/ @MatsNL
|
||||
/tests/components/atag/ @MatsNL
|
||||
/homeassistant/components/aten_pe/ @mtdcr
|
||||
@@ -422,6 +424,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/emby/ @mezz64
|
||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||
/homeassistant/components/emoncms_history/ @alexandrecuer
|
||||
/tests/components/emoncms_history/ @alexandrecuer
|
||||
/homeassistant/components/emonitor/ @bdraco
|
||||
/tests/components/emonitor/ @bdraco
|
||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||
@@ -438,8 +442,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/enigma2/ @autinerd
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
@@ -511,8 +515,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/forked_daapd/ @uvjustin
|
||||
/tests/components/forked_daapd/ @uvjustin
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @krmarien
|
||||
/tests/components/foscam/ @krmarien
|
||||
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
||||
/tests/components/foscam/ @Foscam-wangzhengyu
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
@@ -862,8 +866,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
/tests/components/linear_garage_door/ @IceBotYT
|
||||
/homeassistant/components/linkplay/ @Velleman
|
||||
/tests/components/linkplay/ @Velleman
|
||||
/homeassistant/components/linux_battery/ @fabaff
|
||||
@@ -1183,6 +1185,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||
/homeassistant/components/point/ @fredrike
|
||||
/tests/components/point/ @fredrike
|
||||
/homeassistant/components/pooldose/ @lmaertin
|
||||
/tests/components/pooldose/ @lmaertin
|
||||
/homeassistant/components/poolsense/ @haemishkyd
|
||||
/tests/components/poolsense/ @haemishkyd
|
||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||
@@ -1417,6 +1421,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/skybell/ @tkdrob
|
||||
/homeassistant/components/slack/ @tkdrob @fletcherau
|
||||
/tests/components/slack/ @tkdrob @fletcherau
|
||||
/homeassistant/components/sleep_as_android/ @tr4nt0r
|
||||
/tests/components/sleep_as_android/ @tr4nt0r
|
||||
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
||||
/tests/components/sleepiq/ @mfugate1 @kbickar
|
||||
/homeassistant/components/slide/ @ualex73
|
||||
@@ -1599,6 +1605,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/todo/ @home-assistant/core
|
||||
/homeassistant/components/todoist/ @boralyl
|
||||
/tests/components/todoist/ @boralyl
|
||||
/homeassistant/components/togrill/ @elupus
|
||||
/tests/components/togrill/ @elupus
|
||||
/homeassistant/components/tolo/ @MatthiasLohr
|
||||
/tests/components/tolo/ @MatthiasLohr
|
||||
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
|
||||
@@ -1613,8 +1621,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tplink_omada/ @MarkGodwin
|
||||
/homeassistant/components/traccar/ @ludeeus
|
||||
/tests/components/traccar/ @ludeeus
|
||||
/homeassistant/components/traccar_server/ @ludeeus
|
||||
/tests/components/traccar_server/ @ludeeus
|
||||
/homeassistant/components/trace/ @home-assistant/core
|
||||
/tests/components/trace/ @home-assistant/core
|
||||
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
||||
|
||||
@@ -14,5 +14,8 @@ Still interested? Then you should take a peek at the [developer documentation](h
|
||||
|
||||
## Feature suggestions
|
||||
|
||||
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
|
||||
We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests.
|
||||
If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub.
|
||||
|
||||
## Issue Tracker
|
||||
|
||||
If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub.
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.7.1
|
||||
RUN pip3 install uv==0.8.9
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ class AuthStore:
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
while new_user.id in self._users:
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
self._users[new_user.id] = new_user
|
||||
|
||||
if credentials is None:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "fritzbox",
|
||||
"name": "FRITZ!Box",
|
||||
"name": "FRITZ!",
|
||||
"integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||
from homeassistant.core import (
|
||||
@@ -26,14 +28,24 @@ from .const import (
|
||||
ATTR_STRUCTURE,
|
||||
ATTR_TASK_NAME,
|
||||
DATA_COMPONENT,
|
||||
DATA_IMAGES,
|
||||
DATA_PREFERENCES,
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_DATA,
|
||||
SERVICE_GENERATE_IMAGE,
|
||||
AITaskEntityFeature,
|
||||
)
|
||||
from .entity import AITaskEntity
|
||||
from .http import async_setup as async_setup_http
|
||||
from .task import GenDataTask, GenDataTaskResult, async_generate_data
|
||||
from .task import (
|
||||
GenDataTask,
|
||||
GenDataTaskResult,
|
||||
GenImageTask,
|
||||
GenImageTaskResult,
|
||||
ImageData,
|
||||
async_generate_data,
|
||||
async_generate_image,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
@@ -41,7 +53,11 @@ __all__ = [
|
||||
"AITaskEntityFeature",
|
||||
"GenDataTask",
|
||||
"GenDataTaskResult",
|
||||
"GenImageTask",
|
||||
"GenImageTaskResult",
|
||||
"ImageData",
|
||||
"async_generate_data",
|
||||
"async_generate_image",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
@@ -78,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||
hass.data[DATA_COMPONENT] = entity_component
|
||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||
hass.data[DATA_IMAGES] = {}
|
||||
await hass.data[DATA_PREFERENCES].async_load()
|
||||
async_setup_http(hass)
|
||||
hass.http.register_view(ImageView)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_DATA,
|
||||
@@ -101,6 +119,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
job_type=HassJobType.Coroutinefunction,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_IMAGE,
|
||||
async_service_generate_image,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||
),
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
job_type=HassJobType.Coroutinefunction,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -115,11 +150,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
|
||||
"""Run the run task service."""
|
||||
"""Run the data task service."""
|
||||
result = await async_generate_data(hass=call.hass, **call.data)
|
||||
return result.as_dict()
|
||||
|
||||
|
||||
async def async_service_generate_image(call: ServiceCall) -> ServiceResponse:
|
||||
"""Run the image task service."""
|
||||
return await async_generate_image(hass=call.hass, **call.data)
|
||||
|
||||
|
||||
class AITaskPreferences:
|
||||
"""AI Task preferences."""
|
||||
|
||||
@@ -164,3 +204,29 @@ class AITaskPreferences:
|
||||
def as_dict(self) -> dict[str, str | None]:
|
||||
"""Get the current preferences."""
|
||||
return {key: getattr(self, key) for key in self.KEYS}
|
||||
|
||||
|
||||
class ImageView(HomeAssistantView):
|
||||
"""View to generated images."""
|
||||
|
||||
url = f"/api/{DOMAIN}/images/{{filename}}"
|
||||
name = f"api:{DOMAIN}/images"
|
||||
requires_auth = False
|
||||
|
||||
async def get(
|
||||
self,
|
||||
request: web.Request,
|
||||
filename: str,
|
||||
) -> web.Response:
|
||||
"""Serve image."""
|
||||
hass = request.app[KEY_HASS]
|
||||
image_storage = hass.data[DATA_IMAGES]
|
||||
image_data = image_storage.get(filename)
|
||||
|
||||
if image_data is None:
|
||||
raise web.HTTPNotFound
|
||||
|
||||
return web.Response(
|
||||
body=image_data.data,
|
||||
content_type=image_data.mime_type,
|
||||
)
|
||||
|
||||
@@ -12,12 +12,18 @@ if TYPE_CHECKING:
|
||||
|
||||
from . import AITaskPreferences
|
||||
from .entity import AITaskEntity
|
||||
from .task import ImageData
|
||||
|
||||
DOMAIN = "ai_task"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||
DATA_IMAGES: HassKey[dict[str, ImageData]] = HassKey(f"{DOMAIN}_images")
|
||||
|
||||
IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
|
||||
MAX_IMAGES = 20
|
||||
|
||||
SERVICE_GENERATE_DATA = "generate_data"
|
||||
SERVICE_GENERATE_IMAGE = "generate_image"
|
||||
|
||||
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||
ATTR_TASK_NAME: Final = "task_name"
|
||||
@@ -38,3 +44,6 @@ class AITaskEntityFeature(IntFlag):
|
||||
|
||||
SUPPORT_ATTACHMENTS = 2
|
||||
"""Support attachments with generate data."""
|
||||
|
||||
GENERATE_IMAGE = 4
|
||||
"""Generate images based on instructions."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||
from .task import GenDataTask, GenDataTaskResult
|
||||
from .task import GenDataTask, GenDataTaskResult, GenImageTask, GenImageTaskResult
|
||||
|
||||
|
||||
class AITaskEntity(RestoreEntity):
|
||||
@@ -57,7 +57,7 @@ class AITaskEntity(RestoreEntity):
|
||||
async def _async_get_ai_task_chat_log(
|
||||
self,
|
||||
session: ChatSession,
|
||||
task: GenDataTask,
|
||||
task: GenDataTask | GenImageTask,
|
||||
) -> AsyncGenerator[ChatLog]:
|
||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||
@@ -104,3 +104,23 @@ class AITaskEntity(RestoreEntity):
|
||||
) -> GenDataTaskResult:
|
||||
"""Handle a gen data task."""
|
||||
raise NotImplementedError
|
||||
|
||||
@final
|
||||
async def internal_async_generate_image(
|
||||
self,
|
||||
session: ChatSession,
|
||||
task: GenImageTask,
|
||||
) -> GenImageTaskResult:
|
||||
"""Run a gen image task."""
|
||||
self.__last_activity = dt_util.utcnow().isoformat()
|
||||
self.async_write_ha_state()
|
||||
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
|
||||
return await self._async_generate_image(task, chat_log)
|
||||
|
||||
async def _async_generate_image(
|
||||
self,
|
||||
task: GenImageTask,
|
||||
chat_log: ChatLog,
|
||||
) -> GenImageTaskResult:
|
||||
"""Handle a gen image task."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:star-four-points"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"generate_data": {
|
||||
"service": "mdi:file-star-four-points-outline"
|
||||
},
|
||||
"generate_image": {
|
||||
"service": "mdi:star-four-points-box-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"domain": "ai_task",
|
||||
"name": "AI Task",
|
||||
"after_dependencies": ["camera"],
|
||||
"after_dependencies": ["camera", "http"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["conversation", "media_source"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||
"integration_type": "system",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
81
homeassistant/components/ai_task/media_source.py
Normal file
81
homeassistant/components/ai_task/media_source.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Expose images as media sources."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.components.media_source import (
|
||||
BrowseMediaSource,
|
||||
MediaSource,
|
||||
MediaSourceItem,
|
||||
PlayMedia,
|
||||
Unresolvable,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DATA_IMAGES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> ImageMediaSource:
|
||||
"""Set up image media source."""
|
||||
_LOGGER.debug("Setting up image media source")
|
||||
return ImageMediaSource(hass)
|
||||
|
||||
|
||||
class ImageMediaSource(MediaSource):
|
||||
"""Provide images as media sources."""
|
||||
|
||||
name: str = "AI Generated Images"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize ImageMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
image_storage = self.hass.data[DATA_IMAGES]
|
||||
image = image_storage.get(item.identifier)
|
||||
|
||||
if image is None:
|
||||
raise Unresolvable(f"Could not resolve media item: {item.identifier}")
|
||||
|
||||
return PlayMedia(f"/api/{DOMAIN}/images/{item.identifier}", image.mime_type)
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
if item.identifier:
|
||||
raise BrowseError("Unknown item")
|
||||
|
||||
image_storage = self.hass.data[DATA_IMAGES]
|
||||
|
||||
children = [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=filename,
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=image.mime_type,
|
||||
title=image.title or filename,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for filename, image in image_storage.items()
|
||||
]
|
||||
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=None,
|
||||
media_class=MediaClass.APP,
|
||||
media_content_type="",
|
||||
title="AI Generated Images",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.IMAGE,
|
||||
children=children,
|
||||
)
|
||||
@@ -20,7 +20,6 @@ generate_data:
|
||||
supported_features:
|
||||
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
structure:
|
||||
advanced: true
|
||||
required: false
|
||||
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
|
||||
selector:
|
||||
@@ -31,3 +30,30 @@ generate_data:
|
||||
media:
|
||||
accept:
|
||||
- "*"
|
||||
generate_image:
|
||||
fields:
|
||||
task_name:
|
||||
example: "picture of a dog"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
instructions:
|
||||
example: "Generate a high quality square image of a dog on transparent background"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain: ai_task
|
||||
supported_features:
|
||||
- ai_task.AITaskEntityFeature.GENERATE_IMAGE
|
||||
attachments:
|
||||
required: false
|
||||
selector:
|
||||
media:
|
||||
accept:
|
||||
- "*"
|
||||
|
||||
@@ -25,6 +25,28 @@
|
||||
"description": "List of files to attach for multi-modal AI analysis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"generate_image": {
|
||||
"name": "Generate image",
|
||||
"description": "Uses AI to generate image.",
|
||||
"fields": {
|
||||
"task_name": {
|
||||
"name": "Task name",
|
||||
"description": "Name of the task."
|
||||
},
|
||||
"instructions": {
|
||||
"name": "Instructions",
|
||||
"description": "Instructions that explains the image to be generated."
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Entity ID to run the task on."
|
||||
},
|
||||
"attachments": {
|
||||
"name": "Attachments",
|
||||
"description": "List of files to attach for using as references."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
@@ -11,11 +13,22 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import camera, conversation, media_source
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||
from .const import (
|
||||
DATA_COMPONENT,
|
||||
DATA_IMAGES,
|
||||
DATA_PREFERENCES,
|
||||
DOMAIN,
|
||||
IMAGE_EXPIRY_TIME,
|
||||
MAX_IMAGES,
|
||||
AITaskEntityFeature,
|
||||
)
|
||||
|
||||
|
||||
def _save_camera_snapshot(image: camera.Image) -> Path:
|
||||
@@ -29,43 +42,15 @@ def _save_camera_snapshot(image: camera.Image) -> Path:
|
||||
return Path(temp_file.name)
|
||||
|
||||
|
||||
async def async_generate_data(
|
||||
async def _resolve_attachments(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
task_name: str,
|
||||
entity_id: str | None = None,
|
||||
instructions: str,
|
||||
structure: vol.Schema | None = None,
|
||||
session: ChatSession,
|
||||
attachments: list[dict] | None = None,
|
||||
) -> GenDataTaskResult:
|
||||
"""Run a task in the AI Task integration."""
|
||||
if entity_id is None:
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
|
||||
|
||||
if entity_id is None:
|
||||
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support generating data"
|
||||
)
|
||||
|
||||
# Resolve attachments
|
||||
) -> list[conversation.Attachment]:
|
||||
"""Resolve attachments for a task."""
|
||||
resolved_attachments: list[conversation.Attachment] = []
|
||||
created_files: list[Path] = []
|
||||
|
||||
if (
|
||||
attachments
|
||||
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support attachments"
|
||||
)
|
||||
|
||||
for attachment in attachments or []:
|
||||
media_content_id = attachment["media_content_id"]
|
||||
|
||||
@@ -104,20 +89,59 @@ async def async_generate_data(
|
||||
)
|
||||
)
|
||||
|
||||
if not created_files:
|
||||
return resolved_attachments
|
||||
|
||||
def cleanup_files() -> None:
|
||||
"""Cleanup temporary files."""
|
||||
for file in created_files:
|
||||
file.unlink(missing_ok=True)
|
||||
|
||||
@callback
|
||||
def cleanup_files_callback() -> None:
|
||||
"""Cleanup temporary files."""
|
||||
hass.async_add_executor_job(cleanup_files)
|
||||
|
||||
session.async_on_cleanup(cleanup_files_callback)
|
||||
|
||||
return resolved_attachments
|
||||
|
||||
|
||||
async def async_generate_data(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
task_name: str,
|
||||
entity_id: str | None = None,
|
||||
instructions: str,
|
||||
structure: vol.Schema | None = None,
|
||||
attachments: list[dict] | None = None,
|
||||
) -> GenDataTaskResult:
|
||||
"""Run a data generation task in the AI Task integration."""
|
||||
if entity_id is None:
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
|
||||
|
||||
if entity_id is None:
|
||||
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support generating data"
|
||||
)
|
||||
|
||||
if (
|
||||
attachments
|
||||
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support attachments"
|
||||
)
|
||||
|
||||
with async_get_chat_session(hass) as session:
|
||||
if created_files:
|
||||
|
||||
def cleanup_files() -> None:
|
||||
"""Cleanup temporary files."""
|
||||
for file in created_files:
|
||||
file.unlink(missing_ok=True)
|
||||
|
||||
@callback
|
||||
def cleanup_files_callback() -> None:
|
||||
"""Cleanup temporary files."""
|
||||
hass.async_add_executor_job(cleanup_files)
|
||||
|
||||
session.async_on_cleanup(cleanup_files_callback)
|
||||
resolved_attachments = await _resolve_attachments(hass, session, attachments)
|
||||
|
||||
return await entity.internal_async_generate_data(
|
||||
session,
|
||||
@@ -130,6 +154,97 @@ async def async_generate_data(
|
||||
)
|
||||
|
||||
|
||||
def _cleanup_images(image_storage: dict[str, ImageData], num_to_remove: int) -> None:
|
||||
"""Remove old images to keep the storage size under the limit."""
|
||||
if num_to_remove <= 0:
|
||||
return
|
||||
|
||||
if num_to_remove >= len(image_storage):
|
||||
image_storage.clear()
|
||||
return
|
||||
|
||||
sorted_images = sorted(
|
||||
image_storage.items(),
|
||||
key=lambda item: item[1].timestamp,
|
||||
)
|
||||
|
||||
for filename, _ in sorted_images[:num_to_remove]:
|
||||
image_storage.pop(filename, None)
|
||||
|
||||
|
||||
async def async_generate_image(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
task_name: str,
|
||||
entity_id: str,
|
||||
instructions: str,
|
||||
attachments: list[dict] | None = None,
|
||||
) -> ServiceResponse:
|
||||
"""Run an image generation task in the AI Task integration."""
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_IMAGE not in entity.supported_features:
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support generating images"
|
||||
)
|
||||
|
||||
if (
|
||||
attachments
|
||||
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support attachments"
|
||||
)
|
||||
|
||||
with async_get_chat_session(hass) as session:
|
||||
resolved_attachments = await _resolve_attachments(hass, session, attachments)
|
||||
|
||||
task_result = await entity.internal_async_generate_image(
|
||||
session,
|
||||
GenImageTask(
|
||||
name=task_name,
|
||||
instructions=instructions,
|
||||
attachments=resolved_attachments or None,
|
||||
),
|
||||
)
|
||||
|
||||
service_result = task_result.as_dict()
|
||||
image_data = service_result.pop("image_data")
|
||||
if service_result.get("revised_prompt") is None:
|
||||
service_result["revised_prompt"] = instructions
|
||||
|
||||
image_storage = hass.data[DATA_IMAGES]
|
||||
|
||||
if len(image_storage) + 1 > MAX_IMAGES:
|
||||
_cleanup_images(image_storage, len(image_storage) + 1 - MAX_IMAGES)
|
||||
|
||||
current_time = datetime.now()
|
||||
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
|
||||
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
|
||||
filename = f"{current_time.strftime('%Y-%m-%d_%H%M%S')}_{sanitized_task_name}{ext}"
|
||||
|
||||
image_storage[filename] = ImageData(
|
||||
data=image_data,
|
||||
timestamp=int(current_time.timestamp()),
|
||||
mime_type=task_result.mime_type,
|
||||
title=service_result["revised_prompt"],
|
||||
)
|
||||
|
||||
def _purge_image(filename: str, now: datetime) -> None:
|
||||
"""Remove image from storage."""
|
||||
image_storage.pop(filename, None)
|
||||
|
||||
if IMAGE_EXPIRY_TIME > 0:
|
||||
async_call_later(hass, IMAGE_EXPIRY_TIME, partial(_purge_image, filename))
|
||||
|
||||
service_result["url"] = get_url(hass) + f"/api/{DOMAIN}/images/{filename}"
|
||||
service_result["media_source_id"] = f"media-source://{DOMAIN}/images/{filename}"
|
||||
|
||||
return service_result
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenDataTask:
|
||||
"""Gen data task to be processed."""
|
||||
@@ -167,3 +282,80 @@ class GenDataTaskResult:
|
||||
"conversation_id": self.conversation_id,
|
||||
"data": self.data,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenImageTask:
|
||||
"""Gen image task to be processed."""
|
||||
|
||||
name: str
|
||||
"""Name of the task."""
|
||||
|
||||
instructions: str
|
||||
"""Instructions on what needs to be done."""
|
||||
|
||||
attachments: list[conversation.Attachment] | None = None
|
||||
"""List of attachments to go along the instructions."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return task as a string."""
|
||||
return f"<GenImageTask {self.name}: {id(self)}>"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenImageTaskResult:
|
||||
"""Result of gen image task."""
|
||||
|
||||
image_data: bytes
|
||||
"""Raw image data generated by the model."""
|
||||
|
||||
conversation_id: str
|
||||
"""Unique identifier for the conversation."""
|
||||
|
||||
mime_type: str
|
||||
"""MIME type of the generated image."""
|
||||
|
||||
width: int | None = None
|
||||
"""Width of the generated image, if available."""
|
||||
|
||||
height: int | None = None
|
||||
"""Height of the generated image, if available."""
|
||||
|
||||
model: str | None = None
|
||||
"""Model used to generate the image, if available."""
|
||||
|
||||
revised_prompt: str | None = None
|
||||
"""Revised prompt used to generate the image, if applicable."""
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return result as a dict."""
|
||||
return {
|
||||
"image_data": self.image_data,
|
||||
"conversation_id": self.conversation_id,
|
||||
"mime_type": self.mime_type,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"model": self.model,
|
||||
"revised_prompt": self.revised_prompt,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ImageData:
|
||||
"""Image data for stored generated images."""
|
||||
|
||||
data: bytes
|
||||
"""Raw image data."""
|
||||
|
||||
timestamp: int
|
||||
"""Timestamp when the image was generated, as a Unix timestamp."""
|
||||
|
||||
mime_type: str
|
||||
"""MIME type of the image."""
|
||||
|
||||
title: str
|
||||
"""Title of the image, usually the prompt used to generate it."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return image data as a string."""
|
||||
return f"<ImageData {self.title}: {id(self)}>"
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"display_pm_standard": {
|
||||
"name": "Display PM standard",
|
||||
"state": {
|
||||
"ugm3": "µg/m³",
|
||||
"ugm3": "μg/m³",
|
||||
"us_aqi": "US AQI"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from airos.airos8 import AirOS
|
||||
from airos.airos8 import AirOS8
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -10,7 +10,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
@@ -20,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(hass, verify_ssl=False)
|
||||
|
||||
airos_device = AirOS(
|
||||
airos_device = AirOS8(
|
||||
host=entry.data[CONF_HOST],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
|
||||
106
homeassistant/components/airos/binary_sensor.py
Normal file
106
homeassistant/components/airos/binary_sensor.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""AirOS Binary Sensor component for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
from .entity import AirOSEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe an AirOS binary sensor."""
|
||||
|
||||
value_fn: Callable[[AirOS8Data], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="portfw",
|
||||
translation_key="port_forwarding",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.portfw,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp_client",
|
||||
translation_key="dhcp_client",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcpc,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp_server",
|
||||
translation_key="dhcp_server",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcpd,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="dhcp6_server",
|
||||
translation_key="dhcp6_server",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.dhcp6d_stateful,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSBinarySensorEntityDescription(
|
||||
key="pppoe",
|
||||
translation_key="pppoe",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.services.pppoe,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirOSConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the AirOS binary sensors from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
entity_description: AirOSBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirOSDataUpdateCoordinator,
|
||||
description: AirOSBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the binary sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
@@ -6,11 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
KeyDataMissingError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirOS
|
||||
from .coordinator import AirOS8
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,7 +48,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||
|
||||
airos_device = AirOS(
|
||||
airos_device = AirOS8(
|
||||
host=user_input[CONF_HOST],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
ConnectionSetupError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (ConnectionAuthenticationError, DataMissingError):
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except KeyDataMissingError:
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -4,12 +4,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.airos8 import AirOS8, AirOS8Data
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -24,13 +24,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
||||
"""Class to manage fetching AirOS data from single endpoint."""
|
||||
|
||||
config_entry: AirOSConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS8
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.airos_device = airos_device
|
||||
@@ -42,23 +42,27 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> AirOSData:
|
||||
async def _async_update_data(self) -> AirOS8Data:
|
||||
"""Fetch data from AirOS."""
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (ConnectionAuthenticationError,) as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (DataMissingError,) as err:
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.1"]
|
||||
"requirements": ["airos==0.5.1"]
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ rules:
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: todo
|
||||
comment: prepared binary_sensors will provide this
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from airos.data import NetRole, WirelessMode
|
||||
from airos.data import DerivedWirelessMode, DerivedWirelessRole, NetRole
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -19,25 +19,30 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
UnitOfDataRate,
|
||||
UnitOfFrequency,
|
||||
UnitOfLength,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
from .entity import AirOSEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode]
|
||||
NETROLE_OPTIONS = [mode.value for mode in NetRole]
|
||||
WIRELESS_MODE_OPTIONS = [mode.value for mode in DerivedWirelessMode]
|
||||
WIRELESS_ROLE_OPTIONS = [mode.value for mode in DerivedWirelessRole]
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirOSSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describe an AirOS sensor."""
|
||||
|
||||
value_fn: Callable[[AirOSData], StateType]
|
||||
value_fn: Callable[[AirOS8Data], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
@@ -46,6 +51,7 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="host_cpuload",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda data: data.host.cpuload,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -69,13 +75,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
@@ -90,6 +89,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.throughput.tx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -98,6 +99,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.throughput.rx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -106,6 +109,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.polling.dl_capacity,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
@@ -114,8 +119,45 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||
value_fn=lambda data: data.wireless.polling.ul_capacity,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="host_uptime",
|
||||
translation_key="host_uptime",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
suggested_display_precision=0,
|
||||
suggested_unit_of_measurement=UnitOfTime.DAYS,
|
||||
value_fn=lambda data: data.host.uptime,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_distance",
|
||||
translation_key="wireless_distance",
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
suggested_display_precision=1,
|
||||
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
value_fn=lambda data: data.wireless.distance,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.derived.mode.value,
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_role",
|
||||
translation_key="wireless_role",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.derived.role.value,
|
||||
options=WIRELESS_ROLE_OPTIONS,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,23 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"port_forwarding": {
|
||||
"name": "Port forwarding"
|
||||
},
|
||||
"dhcp_client": {
|
||||
"name": "DHCP client"
|
||||
},
|
||||
"dhcp_server": {
|
||||
"name": "DHCP server"
|
||||
},
|
||||
"dhcp6_server": {
|
||||
"name": "DHCPv6 server"
|
||||
},
|
||||
"pppoe": {
|
||||
"name": "PPPoE link"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"host_cpuload": {
|
||||
"name": "CPU load"
|
||||
@@ -43,13 +60,6 @@
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"ap_ptp": "Access point",
|
||||
"sta_ptp": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
@@ -67,6 +77,26 @@
|
||||
},
|
||||
"wireless_remote_hostname": {
|
||||
"name": "Remote hostname"
|
||||
},
|
||||
"host_uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"wireless_distance": {
|
||||
"name": "Wireless distance"
|
||||
},
|
||||
"wireless_role": {
|
||||
"name": "Wireless role",
|
||||
"state": {
|
||||
"access_point": "Access point",
|
||||
"station": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"point_to_point": "Point-to-point",
|
||||
"point_to_multipoint": "Point-to-multipoint"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE
|
||||
from .coordinator import AirQCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
|
||||
|
||||
AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
return_average=self.return_average,
|
||||
clip_negative_values=self.clip_negative,
|
||||
)
|
||||
data["brightness"] = await self.airq.get_current_brightness()
|
||||
if warming_up_sensors := identify_warming_up_sensors(data):
|
||||
_LOGGER.debug(
|
||||
"Following sensors are still warming up: %s", warming_up_sensors
|
||||
|
||||
85
homeassistant/components/airq/number.py
Normal file
85
homeassistant/components/airq/number.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Definition of air-Q number platform used to control the LED strips."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aioairq.core import AirQ
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirQConfigEntry, AirQCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirQBrightnessDescription(NumberEntityDescription):
|
||||
"""Describes AirQ number entity responsible for brightness control."""
|
||||
|
||||
value: Callable[[dict], float]
|
||||
set_value: Callable[[AirQ, float], Awaitable[None]]
|
||||
|
||||
|
||||
AIRQ_LED_BRIGHTNESS = AirQBrightnessDescription(
|
||||
key="airq_led_brightness",
|
||||
translation_key="airq_led_brightness",
|
||||
native_min_value=0.0,
|
||||
native_max_value=100.0,
|
||||
native_step=1.0,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda data: data["brightness"],
|
||||
set_value=lambda device, value: device.set_current_brightness(value),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AirQConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up number entities: a single entity for the LEDs."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
entities = [AirQLEDBrightness(coordinator, AIRQ_LED_BRIGHTNESS)]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AirQLEDBrightness(CoordinatorEntity[AirQCoordinator], NumberEntity):
|
||||
"""Representation of the LEDs from a single AirQ."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirQCoordinator,
|
||||
description: AirQBrightnessDescription,
|
||||
) -> None:
|
||||
"""Initialize a single sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description: AirQBrightnessDescription = description
|
||||
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the brightness of the LEDs in %."""
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the brightness of the LEDs to the value in %."""
|
||||
_LOGGER.debug(
|
||||
"Changing LED brighntess from %.0f%% to %.0f%%",
|
||||
self.coordinator.data["brightness"],
|
||||
value,
|
||||
)
|
||||
await self.entity_description.set_value(self.coordinator.airq, value)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -35,6 +35,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"airq_led_brightness": {
|
||||
"name": "LED brightness"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"acetaldehyde": {
|
||||
"name": "Acetaldehyde"
|
||||
|
||||
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.7.1"]
|
||||
"requirements": ["aioairzone-cloud==0.7.2"]
|
||||
}
|
||||
|
||||
@@ -2,39 +2,112 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
device_registry as dr,
|
||||
)
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
from . import api
|
||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
"""Set up Aladdin Connect from a config entry."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/aladdin_connect",
|
||||
},
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Aladdin Connect Genie from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
client = AladdinConnectClient(
|
||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||
)
|
||||
|
||||
sdk_doors = await client.get_doors()
|
||||
|
||||
# Convert SDK GarageDoor objects to integration GarageDoor objects
|
||||
doors = [
|
||||
GarageDoor(
|
||||
{
|
||||
"device_id": door.device_id,
|
||||
"door_number": door.door_number,
|
||||
"name": door.name,
|
||||
"status": door.status,
|
||||
"link_status": door.link_status,
|
||||
"battery_level": door.battery_level,
|
||||
}
|
||||
)
|
||||
for door in sdk_doors
|
||||
]
|
||||
|
||||
entry.runtime_data = {
|
||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
||||
for door in doors
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
remove_stale_devices(hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old config."""
|
||||
if config_entry.version < CONFIG_FLOW_VERSION:
|
||||
config_entry.async_start_reauth(hass)
|
||||
new_data = {**config_entry.data}
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new_data,
|
||||
version=CONFIG_FLOW_VERSION,
|
||||
minor_version=CONFIG_FLOW_MINOR_VERSION,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
def remove_stale_devices(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AladdinConnectConfigEntry,
|
||||
) -> None:
|
||||
"""Remove stale devices from device registry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = set(config_entry.runtime_data)
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id and device_id not in all_device_ids:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
33
homeassistant/components/aladdin_connect/api.py
Normal file
33
homeassistant/components/aladdin_connect/api.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from genie_partner_sdk.auth import Auth
|
||||
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
|
||||
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
|
||||
|
||||
|
||||
class AsyncConfigEntryAuth(Auth):
|
||||
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize Aladdin Connect Genie auth."""
|
||||
super().__init__(
|
||||
websession, API_URL, oauth_session.token["access_token"], API_KEY
|
||||
)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
if not self._oauth_session.valid_token:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
@@ -0,0 +1,14 @@
|
||||
"""application_credentials platform the Aladdin Connect Genie integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
return AuthorizationServer(
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
||||
@@ -1,11 +1,63 @@
|
||||
"""Config flow for Aladdin Connect integration."""
|
||||
"""Config flow for Aladdin Connect Genie."""
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from . import DOMAIN
|
||||
import jwt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
||||
|
||||
|
||||
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Aladdin Connect."""
|
||||
class OAuth2FlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
|
||||
|
||||
VERSION = 1
|
||||
DOMAIN = DOMAIN
|
||||
VERSION = CONFIG_FLOW_VERSION
|
||||
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
|
||||
|
||||
async def async_step_reauth(
|
||||
self, user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an oauth config entry or update existing entry for reauth."""
|
||||
# Extract the user ID from the JWT token's 'sub' field
|
||||
token = jwt.decode(
|
||||
data["token"]["access_token"], options={"verify_signature": False}
|
||||
)
|
||||
user_id = token["sub"]
|
||||
await self.async_set_unique_id(user_id)
|
||||
|
||||
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=data
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title="Aladdin Connect", data=data)
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
14
homeassistant/components/aladdin_connect/const.py
Normal file
14
homeassistant/components/aladdin_connect/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants for the Aladdin Connect Genie integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.cover import CoverEntityFeature
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
CONFIG_FLOW_VERSION = 2
|
||||
CONFIG_FLOW_MINOR_VERSION = 1
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
|
||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
||||
|
||||
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
44
homeassistant/components/aladdin_connect/coordinator.py
Normal file
44
homeassistant/components/aladdin_connect/coordinator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Coordinator for Aladdin Connect integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
||||
"""Coordinator for Aladdin Connect integration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
client: AladdinConnectClient,
|
||||
garage_door: GarageDoor,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
config_entry=entry,
|
||||
name="Aladdin Connect Coordinator",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.data = garage_door
|
||||
|
||||
async def _async_update_data(self) -> GarageDoor:
|
||||
"""Fetch data from the Aladdin Connect API."""
|
||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
||||
return self.data
|
||||
62
homeassistant/components/aladdin_connect/cover.py
Normal file
62
homeassistant/components/aladdin_connect/cover.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Cover Entity for Genie Garage Door."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import SUPPORTED_FEATURES
|
||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the cover platform."""
|
||||
coordinators = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
|
||||
)
|
||||
|
||||
|
||||
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
"""Representation of Aladdin Connect cover."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.GARAGE
|
||||
_attr_supported_features = SUPPORTED_FEATURES
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
"""Initialize the Aladdin Connect cover."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.data.unique_id
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue open command to cover."""
|
||||
await self.client.open_door(self._device_id, self._number)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue close command to cover."""
|
||||
await self.client.close_door(self._device_id, self._number)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
return self.coordinator.data.status == "closed"
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool | None:
|
||||
"""Update is closing attribute."""
|
||||
return self.coordinator.data.status == "closing"
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool | None:
|
||||
"""Update is opening attribute."""
|
||||
return self.coordinator.data.status == "opening"
|
||||
32
homeassistant/components/aladdin_connect/entity.py
Normal file
32
homeassistant/components/aladdin_connect/entity.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Base class for Aladdin Connect entities."""
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AladdinConnectCoordinator
|
||||
|
||||
|
||||
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
||||
"""Defines a base Aladdin Connect entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
"""Initialize Aladdin Connect entity."""
|
||||
super().__init__(coordinator)
|
||||
device = coordinator.data
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
manufacturer="Aladdin Connect",
|
||||
name=device.name,
|
||||
)
|
||||
self._device_id = device.device_id
|
||||
self._number = device.door_number
|
||||
|
||||
@property
|
||||
def client(self) -> AladdinConnectClient:
|
||||
"""Return the client for this entity."""
|
||||
return self.coordinator.client
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@swcloudgenie"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"integration_type": "system",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": []
|
||||
"requirements": ["genie-partner-sdk==1.0.10"]
|
||||
}
|
||||
|
||||
94
homeassistant/components/aladdin_connect/quality_scale.yaml
Normal file
94
homeassistant/components/aladdin_connect/quality_scale.yaml
Normal file
@@ -0,0 +1,94 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register any service actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow: done
|
||||
config-flow-test-coverage: todo
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register any service actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-removal-instructions:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Integration does not subscribe to external events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure:
|
||||
status: todo
|
||||
comment: Config flow does not currently test connection during setup.
|
||||
test-before-setup: todo
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-installation-parameters:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: Platform tests for cover and sensor need to be implemented to reach 95% coverage.
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery: todo
|
||||
discovery-update-info: todo
|
||||
docs-data-update:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-examples:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-known-limitations:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-supported-devices:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-supported-functions:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-troubleshooting:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
docs-use-cases:
|
||||
status: todo
|
||||
comment: Documentation needs to be created.
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: Stale devices can be done dynamically
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
77
homeassistant/components/aladdin_connect/sensor.py
Normal file
77
homeassistant/components/aladdin_connect/sensor.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Support for Aladdin Connect Genie sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AladdinConnectSensorEntityDescription(SensorEntityDescription):
|
||||
"""Sensor entity description for Aladdin Connect."""
|
||||
|
||||
value_fn: Callable[[GarageDoor], float | None]
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[AladdinConnectSensorEntityDescription, ...] = (
|
||||
AladdinConnectSensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda garage_door: garage_door.battery_level,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aladdin Connect sensor devices."""
|
||||
coordinators = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
"""A sensor implementation for Aladdin Connect device."""
|
||||
|
||||
entity_description: AladdinConnectSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AladdinConnectCoordinator,
|
||||
entity_description: AladdinConnectSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Aladdin Connect sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
@@ -1,8 +1,30 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The Aladdin Connect integration has been removed",
|
||||
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Aladdin Connect needs to re-authenticate your account"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Support for repeating alerts when conditions are met."""
|
||||
"""Support for repeating alerts when conditions are met.
|
||||
|
||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -63,7 +66,10 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Alert component."""
|
||||
"""Set up the Alert component.
|
||||
|
||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||
"""
|
||||
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
||||
|
||||
entities: list[AlertEntity] = []
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Support for repeating alerts when conditions are met."""
|
||||
"""Support for repeating alerts when conditions are met.
|
||||
|
||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -27,7 +30,10 @@ from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
class AlertEntity(Entity):
|
||||
"""Representation of an alert."""
|
||||
"""Representation of an alert.
|
||||
|
||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||
"""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Reproduce an Alert state."""
|
||||
"""Reproduce an Alert state.
|
||||
|
||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_COUNTRY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
# Convert country in domain
|
||||
country = entry.data[CONF_COUNTRY].lower()
|
||||
domain = COUNTRY_DOMAINS.get(country, country)
|
||||
|
||||
# Add site to login data
|
||||
new_data = entry.data.copy()
|
||||
new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}"
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, version=1, minor_version=2
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Migration to version %s.%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -10,16 +10,14 @@ from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
WrongCountry,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
@@ -29,6 +27,12 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
)
|
||||
STEP_RECONFIGURE = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -37,7 +41,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
api = AmazonEchoApi(
|
||||
session,
|
||||
data[CONF_COUNTRY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
@@ -48,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -58,12 +64,10 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
except (CannotAuthenticate, TypeError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -78,9 +82,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY, default=self.hass.config.country
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
@@ -109,7 +110,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
except (CannotAuthenticate, TypeError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
@@ -129,3 +130,47 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the device."""
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=STEP_RECONFIGURE,
|
||||
)
|
||||
|
||||
updated_password = user_input[CONF_PASSWORD]
|
||||
|
||||
self._async_abort_entries_match(
|
||||
{CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]}
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
data = await validate_input(
|
||||
self.hass, {**reconfigure_entry.data, **user_input}
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={
|
||||
CONF_PASSWORD: updated_password,
|
||||
CONF_LOGIN_DATA: data,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=STEP_RECONFIGURE,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
|
||||
DEFAULT_DOMAIN = "com"
|
||||
COUNTRY_DOMAINS = {
|
||||
"ar": DEFAULT_DOMAIN,
|
||||
"at": DEFAULT_DOMAIN,
|
||||
"au": "com.au",
|
||||
"be": "com.be",
|
||||
"br": DEFAULT_DOMAIN,
|
||||
"gb": "co.uk",
|
||||
"il": DEFAULT_DOMAIN,
|
||||
"jp": "co.jp",
|
||||
"mx": "com.mx",
|
||||
"no": DEFAULT_DOMAIN,
|
||||
"nz": "com.au",
|
||||
"pl": DEFAULT_DOMAIN,
|
||||
"tr": "com.tr",
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": "co.za",
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from aioamazondevices.exceptions import (
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
session,
|
||||
entry.data[CONF_COUNTRY],
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
@@ -67,7 +66,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
except (CannotAuthenticate, TypeError) as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==4.0.0"]
|
||||
"requirements": ["aioamazondevices==6.0.0"]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: no known use cases for repair issues or flows, yet
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -41,11 +42,13 @@ SENSORS: Final = (
|
||||
if device.sensors[_key].scale == "CELSIUS"
|
||||
else UnitOfTemperature.FAHRENHEIT
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="illuminance",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -14,14 +14,12 @@ from .coordinator import AmazonConfigEntry
|
||||
|
||||
ATTR_TEXT_COMMAND = "text_command"
|
||||
ATTR_SOUND = "sound"
|
||||
ATTR_SOUND_VARIANT = "sound_variant"
|
||||
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||
|
||||
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SOUND): cv.string,
|
||||
vol.Required(ATTR_SOUND_VARIANT): cv.positive_int,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
},
|
||||
)
|
||||
@@ -75,17 +73,14 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
if attribute == ATTR_SOUND:
|
||||
variant: int = call.data[ATTR_SOUND_VARIANT]
|
||||
pad = "_" if variant > 10 else "_0"
|
||||
file = f"{value}{pad}{variant!s}"
|
||||
if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]:
|
||||
if value not in SOUNDS_LIST:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_sound_value",
|
||||
translation_placeholders={"sound": value, "variant": str(variant)},
|
||||
translation_placeholders={"sound": value},
|
||||
)
|
||||
await coordinator.api.call_alexa_sound(
|
||||
coordinator.data[device.serial_number], file
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
elif attribute == ATTR_TEXT_COMMAND:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
|
||||
@@ -18,14 +18,6 @@ send_sound:
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
sound_variant:
|
||||
required: true
|
||||
example: 1
|
||||
default: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 50
|
||||
sound:
|
||||
required: true
|
||||
example: amzn_sfx_doorbell_chime
|
||||
@@ -33,472 +25,45 @@ send_sound:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- air_horn
|
||||
- air_horns
|
||||
- airboat
|
||||
- airport
|
||||
- aliens
|
||||
- amzn_sfx_airplane_takeoff_whoosh
|
||||
- amzn_sfx_army_march_clank_7x
|
||||
- amzn_sfx_army_march_large_8x
|
||||
- amzn_sfx_army_march_small_8x
|
||||
- amzn_sfx_baby_big_cry
|
||||
- amzn_sfx_baby_cry
|
||||
- amzn_sfx_baby_fuss
|
||||
- amzn_sfx_battle_group_clanks
|
||||
- amzn_sfx_battle_man_grunts
|
||||
- amzn_sfx_battle_men_grunts
|
||||
- amzn_sfx_battle_men_horses
|
||||
- amzn_sfx_battle_noisy_clanks
|
||||
- amzn_sfx_battle_yells_men
|
||||
- amzn_sfx_battle_yells_men_run
|
||||
- amzn_sfx_bear_groan_roar
|
||||
- amzn_sfx_bear_roar_grumble
|
||||
- amzn_sfx_bear_roar_small
|
||||
- amzn_sfx_beep_1x
|
||||
- amzn_sfx_bell_med_chime
|
||||
- amzn_sfx_bell_short_chime
|
||||
- amzn_sfx_bell_timer
|
||||
- amzn_sfx_bicycle_bell_ring
|
||||
- amzn_sfx_bird_chickadee_chirp_1x
|
||||
- amzn_sfx_bird_chickadee_chirps
|
||||
- amzn_sfx_bird_forest
|
||||
- amzn_sfx_bird_forest_short
|
||||
- amzn_sfx_bird_robin_chirp_1x
|
||||
- amzn_sfx_boing_long_1x
|
||||
- amzn_sfx_boing_med_1x
|
||||
- amzn_sfx_boing_short_1x
|
||||
- amzn_sfx_bus_drive_past
|
||||
- amzn_sfx_buzz_electronic
|
||||
- amzn_sfx_buzzer_loud_alarm
|
||||
- amzn_sfx_buzzer_small
|
||||
- amzn_sfx_car_accelerate
|
||||
- amzn_sfx_car_accelerate_noisy
|
||||
- amzn_sfx_car_click_seatbelt
|
||||
- amzn_sfx_car_close_door_1x
|
||||
- amzn_sfx_car_drive_past
|
||||
- amzn_sfx_car_honk_1x
|
||||
- amzn_sfx_car_honk_2x
|
||||
- amzn_sfx_car_honk_3x
|
||||
- amzn_sfx_car_honk_long_1x
|
||||
- amzn_sfx_car_into_driveway
|
||||
- amzn_sfx_car_into_driveway_fast
|
||||
- amzn_sfx_car_slam_door_1x
|
||||
- amzn_sfx_car_undo_seatbelt
|
||||
- amzn_sfx_cat_angry_meow_1x
|
||||
- amzn_sfx_cat_angry_screech_1x
|
||||
- amzn_sfx_cat_long_meow_1x
|
||||
- amzn_sfx_cat_meow_1x
|
||||
- amzn_sfx_cat_purr
|
||||
- amzn_sfx_cat_purr_meow
|
||||
- amzn_sfx_chicken_cluck
|
||||
- amzn_sfx_church_bell_1x
|
||||
- amzn_sfx_church_bells_ringing
|
||||
- amzn_sfx_clear_throat_ahem
|
||||
- amzn_sfx_clock_ticking
|
||||
- amzn_sfx_clock_ticking_long
|
||||
- amzn_sfx_copy_machine
|
||||
- amzn_sfx_cough
|
||||
- amzn_sfx_crow_caw_1x
|
||||
- amzn_sfx_crowd_applause
|
||||
- amzn_sfx_crowd_bar
|
||||
- amzn_sfx_crowd_bar_rowdy
|
||||
- amzn_sfx_crowd_boo
|
||||
- amzn_sfx_crowd_cheer_med
|
||||
- amzn_sfx_crowd_excited_cheer
|
||||
- amzn_sfx_dog_med_bark_1x
|
||||
- amzn_sfx_dog_med_bark_2x
|
||||
- amzn_sfx_dog_med_bark_growl
|
||||
- amzn_sfx_dog_med_growl_1x
|
||||
- amzn_sfx_dog_med_woof_1x
|
||||
- amzn_sfx_dog_small_bark_2x
|
||||
- amzn_sfx_door_open
|
||||
- amzn_sfx_door_shut
|
||||
- amzn_sfx_doorbell
|
||||
- amzn_sfx_doorbell_buzz
|
||||
- amzn_sfx_doorbell_chime
|
||||
- amzn_sfx_drinking_slurp
|
||||
- amzn_sfx_drum_and_cymbal
|
||||
- amzn_sfx_drum_comedy
|
||||
- amzn_sfx_earthquake_rumble
|
||||
- amzn_sfx_electric_guitar
|
||||
- amzn_sfx_electronic_beep
|
||||
- amzn_sfx_electronic_major_chord
|
||||
- amzn_sfx_elephant
|
||||
- amzn_sfx_elevator_bell_1x
|
||||
- amzn_sfx_elevator_open_bell
|
||||
- amzn_sfx_fairy_melodic_chimes
|
||||
- amzn_sfx_fairy_sparkle_chimes
|
||||
- amzn_sfx_faucet_drip
|
||||
- amzn_sfx_faucet_running
|
||||
- amzn_sfx_fireplace_crackle
|
||||
- amzn_sfx_fireworks
|
||||
- amzn_sfx_fireworks_firecrackers
|
||||
- amzn_sfx_fireworks_launch
|
||||
- amzn_sfx_fireworks_whistles
|
||||
- amzn_sfx_food_frying
|
||||
- amzn_sfx_footsteps
|
||||
- amzn_sfx_footsteps_muffled
|
||||
- amzn_sfx_ghost_spooky
|
||||
- amzn_sfx_glass_on_table
|
||||
- amzn_sfx_glasses_clink
|
||||
- amzn_sfx_horse_gallop_4x
|
||||
- amzn_sfx_horse_huff_whinny
|
||||
- amzn_sfx_horse_neigh
|
||||
- amzn_sfx_horse_neigh_low
|
||||
- amzn_sfx_horse_whinny
|
||||
- amzn_sfx_human_walking
|
||||
- amzn_sfx_jar_on_table_1x
|
||||
- amzn_sfx_kitchen_ambience
|
||||
- amzn_sfx_large_crowd_cheer
|
||||
- amzn_sfx_large_fire_crackling
|
||||
- amzn_sfx_laughter
|
||||
- amzn_sfx_laughter_giggle
|
||||
- amzn_sfx_lightning_strike
|
||||
- amzn_sfx_lion_roar
|
||||
- amzn_sfx_magic_blast_1x
|
||||
- amzn_sfx_monkey_calls_3x
|
||||
- amzn_sfx_monkey_chimp
|
||||
- amzn_sfx_monkeys_chatter
|
||||
- amzn_sfx_motorcycle_accelerate
|
||||
- amzn_sfx_motorcycle_engine_idle
|
||||
- amzn_sfx_motorcycle_engine_rev
|
||||
- amzn_sfx_musical_drone_intro
|
||||
- amzn_sfx_oars_splashing_rowboat
|
||||
- amzn_sfx_object_on_table_2x
|
||||
- amzn_sfx_ocean_wave_1x
|
||||
- amzn_sfx_ocean_wave_on_rocks_1x
|
||||
- amzn_sfx_ocean_wave_surf
|
||||
- amzn_sfx_people_walking
|
||||
- amzn_sfx_person_running
|
||||
- amzn_sfx_piano_note_1x
|
||||
- amzn_sfx_punch
|
||||
- amzn_sfx_rain
|
||||
- amzn_sfx_rain_on_roof
|
||||
- amzn_sfx_rain_thunder
|
||||
- amzn_sfx_rat_squeak_2x
|
||||
- amzn_sfx_rat_squeaks
|
||||
- amzn_sfx_raven_caw_1x
|
||||
- amzn_sfx_raven_caw_2x
|
||||
- amzn_sfx_restaurant_ambience
|
||||
- amzn_sfx_rooster_crow
|
||||
- amzn_sfx_scifi_air_escaping
|
||||
- amzn_sfx_scifi_alarm
|
||||
- amzn_sfx_scifi_alien_voice
|
||||
- amzn_sfx_scifi_boots_walking
|
||||
- amzn_sfx_scifi_close_large_explosion
|
||||
- amzn_sfx_scifi_door_open
|
||||
- amzn_sfx_scifi_engines_on
|
||||
- amzn_sfx_scifi_engines_on_large
|
||||
- amzn_sfx_scifi_engines_on_short_burst
|
||||
- amzn_sfx_scifi_explosion
|
||||
- amzn_sfx_scifi_explosion_2x
|
||||
- amzn_sfx_scifi_incoming_explosion
|
||||
- amzn_sfx_scifi_laser_gun_battle
|
||||
- amzn_sfx_scifi_laser_gun_fires
|
||||
- amzn_sfx_scifi_laser_gun_fires_large
|
||||
- amzn_sfx_scifi_long_explosion_1x
|
||||
- amzn_sfx_scifi_missile
|
||||
- amzn_sfx_scifi_motor_short_1x
|
||||
- amzn_sfx_scifi_open_airlock
|
||||
- amzn_sfx_scifi_radar_high_ping
|
||||
- amzn_sfx_scifi_radar_low
|
||||
- amzn_sfx_scifi_radar_medium
|
||||
- amzn_sfx_scifi_run_away
|
||||
- amzn_sfx_scifi_sheilds_up
|
||||
- amzn_sfx_scifi_short_low_explosion
|
||||
- amzn_sfx_scifi_small_whoosh_flyby
|
||||
- amzn_sfx_scifi_small_zoom_flyby
|
||||
- amzn_sfx_scifi_sonar_ping_3x
|
||||
- amzn_sfx_scifi_sonar_ping_4x
|
||||
- amzn_sfx_scifi_spaceship_flyby
|
||||
- amzn_sfx_scifi_timer_beep
|
||||
- amzn_sfx_scifi_zap_backwards
|
||||
- amzn_sfx_scifi_zap_electric
|
||||
- amzn_sfx_sheep_baa
|
||||
- amzn_sfx_sheep_bleat
|
||||
- amzn_sfx_silverware_clank
|
||||
- amzn_sfx_sirens
|
||||
- amzn_sfx_sleigh_bells
|
||||
- amzn_sfx_small_stream
|
||||
- amzn_sfx_sneeze
|
||||
- amzn_sfx_stream
|
||||
- amzn_sfx_strong_wind_desert
|
||||
- amzn_sfx_strong_wind_whistling
|
||||
- amzn_sfx_subway_leaving
|
||||
- amzn_sfx_subway_passing
|
||||
- amzn_sfx_subway_stopping
|
||||
- amzn_sfx_swoosh_cartoon_fast
|
||||
- amzn_sfx_swoosh_fast_1x
|
||||
- amzn_sfx_swoosh_fast_6x
|
||||
- amzn_sfx_test_tone
|
||||
- amzn_sfx_thunder_rumble
|
||||
- amzn_sfx_toilet_flush
|
||||
- amzn_sfx_trumpet_bugle
|
||||
- amzn_sfx_turkey_gobbling
|
||||
- amzn_sfx_typing_medium
|
||||
- amzn_sfx_typing_short
|
||||
- amzn_sfx_typing_typewriter
|
||||
- amzn_sfx_vacuum_off
|
||||
- amzn_sfx_vacuum_on
|
||||
- amzn_sfx_walking_in_mud
|
||||
- amzn_sfx_walking_in_snow
|
||||
- amzn_sfx_walking_on_grass
|
||||
- amzn_sfx_water_dripping
|
||||
- amzn_sfx_water_droplets
|
||||
- amzn_sfx_wind_strong_gusting
|
||||
- amzn_sfx_wind_whistling_desert
|
||||
- amzn_sfx_wings_flap_4x
|
||||
- amzn_sfx_wings_flap_fast
|
||||
- amzn_sfx_wolf_howl
|
||||
- amzn_sfx_wolf_young_howl
|
||||
- amzn_sfx_wooden_door
|
||||
- amzn_sfx_wooden_door_creaks_long
|
||||
- amzn_sfx_wooden_door_creaks_multiple
|
||||
- amzn_sfx_wooden_door_creaks_open
|
||||
- amzn_ui_sfx_gameshow_bridge
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_32s_full
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_64s_full
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_64s_minimal
|
||||
- amzn_ui_sfx_gameshow_intro
|
||||
- amzn_ui_sfx_gameshow_negative_response
|
||||
- amzn_ui_sfx_gameshow_neutral_response
|
||||
- amzn_ui_sfx_gameshow_outro
|
||||
- amzn_ui_sfx_gameshow_player1
|
||||
- amzn_ui_sfx_gameshow_player2
|
||||
- amzn_ui_sfx_gameshow_player3
|
||||
- amzn_ui_sfx_gameshow_player4
|
||||
- amzn_ui_sfx_gameshow_positive_response
|
||||
- amzn_ui_sfx_gameshow_tally_negative
|
||||
- amzn_ui_sfx_gameshow_tally_positive
|
||||
- amzn_ui_sfx_gameshow_waiting_loop_30s
|
||||
- anchor
|
||||
- answering_machines
|
||||
- arcs_sparks
|
||||
- arrows_bows
|
||||
- baby
|
||||
- back_up_beeps
|
||||
- bars_restaurants
|
||||
- baseball
|
||||
- basketball
|
||||
- battles
|
||||
- beeps_tones
|
||||
- bell
|
||||
- bikes
|
||||
- billiards
|
||||
- board_games
|
||||
- body
|
||||
- boing
|
||||
- books
|
||||
- bow_wash
|
||||
- box
|
||||
- break_shatter_smash
|
||||
- breaks
|
||||
- brooms_mops
|
||||
- bullets
|
||||
- buses
|
||||
- buzz
|
||||
- buzz_hums
|
||||
- buzzers
|
||||
- buzzers_pistols
|
||||
- cables_metal
|
||||
- camera
|
||||
- cannons
|
||||
- car_alarm
|
||||
- car_alarms
|
||||
- car_cell_phones
|
||||
- carnivals_fairs
|
||||
- cars
|
||||
- casino
|
||||
- casinos
|
||||
- cellar
|
||||
- chimes
|
||||
- chimes_bells
|
||||
- chorus
|
||||
- christmas
|
||||
- church_bells
|
||||
- clock
|
||||
- cloth
|
||||
- concrete
|
||||
- construction
|
||||
- construction_factory
|
||||
- crashes
|
||||
- crowds
|
||||
- debris
|
||||
- dining_kitchens
|
||||
- dinosaurs
|
||||
- dripping
|
||||
- drops
|
||||
- electric
|
||||
- electrical
|
||||
- elevator
|
||||
- evolution_monsters
|
||||
- explosions
|
||||
- factory
|
||||
- falls
|
||||
- fax_scanner_copier
|
||||
- feedback_mics
|
||||
- fight
|
||||
- fire
|
||||
- fire_extinguisher
|
||||
- fireballs
|
||||
- fireworks
|
||||
- fishing_pole
|
||||
- flags
|
||||
- football
|
||||
- footsteps
|
||||
- futuristic
|
||||
- futuristic_ship
|
||||
- gameshow
|
||||
- gear
|
||||
- ghosts_demons
|
||||
- giant_monster
|
||||
- glass
|
||||
- glasses_clink
|
||||
- golf
|
||||
- gorilla
|
||||
- grenade_lanucher
|
||||
- griffen
|
||||
- gyms_locker_rooms
|
||||
- handgun_loading
|
||||
- handgun_shot
|
||||
- handle
|
||||
- hands
|
||||
- heartbeats_ekg
|
||||
- helicopter
|
||||
- high_tech
|
||||
- hit_punch_slap
|
||||
- hits
|
||||
- horns
|
||||
- horror
|
||||
- hot_tub_filling_up
|
||||
- human
|
||||
- human_vocals
|
||||
- hygene # codespell:ignore
|
||||
- ice_skating
|
||||
- ignitions
|
||||
- infantry
|
||||
- intro
|
||||
- jet
|
||||
- juggling
|
||||
- key_lock
|
||||
- kids
|
||||
- knocks
|
||||
- lab_equip
|
||||
- lacrosse
|
||||
- lamps_lanterns
|
||||
- leather
|
||||
- liquid_suction
|
||||
- locker_doors
|
||||
- machine_gun
|
||||
- magic_spells
|
||||
- medium_large_explosions
|
||||
- metal
|
||||
- modern_rings
|
||||
- money_coins
|
||||
- motorcycles
|
||||
- movement
|
||||
- moves
|
||||
- nature
|
||||
- oar_boat
|
||||
- pagers
|
||||
- paintball
|
||||
- paper
|
||||
- parachute
|
||||
- pay_phones
|
||||
- phone_beeps
|
||||
- pigmy_bats
|
||||
- pills
|
||||
- pour_water
|
||||
- power_up_down
|
||||
- printers
|
||||
- prison
|
||||
- public_space
|
||||
- racquetball
|
||||
- radios_static
|
||||
- rain
|
||||
- rc_airplane
|
||||
- rc_car
|
||||
- refrigerators_freezers
|
||||
- regular
|
||||
- respirator
|
||||
- rifle
|
||||
- roller_coaster
|
||||
- rollerskates_rollerblades
|
||||
- room_tones
|
||||
- ropes_climbing
|
||||
- rotary_rings
|
||||
- rowboat_canoe
|
||||
- rubber
|
||||
- running
|
||||
- sails
|
||||
- sand_gravel
|
||||
- screen_doors
|
||||
- screens
|
||||
- seats_stools
|
||||
- servos
|
||||
- shoes_boots
|
||||
- shotgun
|
||||
- shower
|
||||
- sink_faucet
|
||||
- sink_filling_water
|
||||
- sink_run_and_off
|
||||
- sink_water_splatter
|
||||
- sirens
|
||||
- skateboards
|
||||
- ski
|
||||
- skids_tires
|
||||
- sled
|
||||
- slides
|
||||
- small_explosions
|
||||
- snow
|
||||
- snowmobile
|
||||
- soldiers
|
||||
- splash_water
|
||||
- splashes_sprays
|
||||
- sports_whistles
|
||||
- squeaks
|
||||
- squeaky
|
||||
- stairs
|
||||
- steam
|
||||
- submarine_diesel
|
||||
- swing_doors
|
||||
- switches_levers
|
||||
- swords
|
||||
- tape
|
||||
- tape_machine
|
||||
- televisions_shows
|
||||
- tennis_pingpong
|
||||
- textile
|
||||
- throw
|
||||
- thunder
|
||||
- ticks
|
||||
- timer
|
||||
- toilet_flush
|
||||
- tone
|
||||
- tones_noises
|
||||
- toys
|
||||
- tractors
|
||||
- traffic
|
||||
- train
|
||||
- trucks_vans
|
||||
- turnstiles
|
||||
- typing
|
||||
- umbrella
|
||||
- underwater
|
||||
- vampires
|
||||
- various
|
||||
- video_tunes
|
||||
- volcano_earthquake
|
||||
- watches
|
||||
- water
|
||||
- water_running
|
||||
- werewolves
|
||||
- winches_gears
|
||||
- wind
|
||||
- wood
|
||||
- wood_boat
|
||||
- woosh
|
||||
- zap
|
||||
- zippers
|
||||
- air_horn_03
|
||||
- amzn_sfx_cat_meow_1x_01
|
||||
- amzn_sfx_church_bell_1x_02
|
||||
- amzn_sfx_crowd_applause_01
|
||||
- amzn_sfx_dog_med_bark_1x_02
|
||||
- amzn_sfx_doorbell_01
|
||||
- amzn_sfx_doorbell_chime_01
|
||||
- amzn_sfx_doorbell_chime_02
|
||||
- amzn_sfx_large_crowd_cheer_01
|
||||
- amzn_sfx_lion_roar_02
|
||||
- amzn_sfx_rooster_crow_01
|
||||
- amzn_sfx_scifi_alarm_01
|
||||
- amzn_sfx_scifi_alarm_04
|
||||
- amzn_sfx_scifi_engines_on_02
|
||||
- amzn_sfx_scifi_sheilds_up_01
|
||||
- amzn_sfx_trumpet_bugle_04
|
||||
- amzn_sfx_wolf_howl_02
|
||||
- bell_02
|
||||
- boing_01
|
||||
- boing_03
|
||||
- buzzers_pistols_01
|
||||
- camera_01
|
||||
- christmas_05
|
||||
- clock_01
|
||||
- futuristic_10
|
||||
- halloween_bats
|
||||
- halloween_crows
|
||||
- halloween_footsteps
|
||||
- halloween_wind
|
||||
- halloween_wolf
|
||||
- holiday_halloween_ghost
|
||||
- horror_10
|
||||
- med_system_alerts_minimal_dragon_short
|
||||
- med_system_alerts_minimal_owl_short
|
||||
- med_system_alerts_minimals_blue_wave_small
|
||||
- med_system_alerts_minimals_galaxy_short
|
||||
- med_system_alerts_minimals_panda_short
|
||||
- med_system_alerts_minimals_tiger_short
|
||||
- med_ui_success_generic_1-1
|
||||
- squeaky_12
|
||||
- zap_01
|
||||
translation_key: sound
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"data_description_country": "The country where your Amazon account is registered.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
|
||||
@@ -12,13 +11,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
@@ -33,6 +30,16 @@
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
@@ -40,13 +47,13 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
@@ -122,474 +129,47 @@
|
||||
"selector": {
|
||||
"sound": {
|
||||
"options": {
|
||||
"air_horn": "Air Horn",
|
||||
"air_horns": "Air Horns",
|
||||
"airboat": "Airboat",
|
||||
"airport": "Airport",
|
||||
"aliens": "Aliens",
|
||||
"amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh",
|
||||
"amzn_sfx_army_march_clank_7x": "Army March Clank 7x",
|
||||
"amzn_sfx_army_march_large_8x": "Army March Large 8x",
|
||||
"amzn_sfx_army_march_small_8x": "Army March Small 8x",
|
||||
"amzn_sfx_baby_big_cry": "Baby Big Cry",
|
||||
"amzn_sfx_baby_cry": "Baby Cry",
|
||||
"amzn_sfx_baby_fuss": "Baby Fuss",
|
||||
"amzn_sfx_battle_group_clanks": "Battle Group Clanks",
|
||||
"amzn_sfx_battle_man_grunts": "Battle Man Grunts",
|
||||
"amzn_sfx_battle_men_grunts": "Battle Men Grunts",
|
||||
"amzn_sfx_battle_men_horses": "Battle Men Horses",
|
||||
"amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks",
|
||||
"amzn_sfx_battle_yells_men": "Battle Yells Men",
|
||||
"amzn_sfx_battle_yells_men_run": "Battle Yells Men Run",
|
||||
"amzn_sfx_bear_groan_roar": "Bear Groan Roar",
|
||||
"amzn_sfx_bear_roar_grumble": "Bear Roar Grumble",
|
||||
"amzn_sfx_bear_roar_small": "Bear Roar Small",
|
||||
"amzn_sfx_beep_1x": "Beep 1x",
|
||||
"amzn_sfx_bell_med_chime": "Bell Med Chime",
|
||||
"amzn_sfx_bell_short_chime": "Bell Short Chime",
|
||||
"amzn_sfx_bell_timer": "Bell Timer",
|
||||
"amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring",
|
||||
"amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x",
|
||||
"amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps",
|
||||
"amzn_sfx_bird_forest": "Bird Forest",
|
||||
"amzn_sfx_bird_forest_short": "Bird Forest Short",
|
||||
"amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x",
|
||||
"amzn_sfx_boing_long_1x": "Boing Long 1x",
|
||||
"amzn_sfx_boing_med_1x": "Boing Med 1x",
|
||||
"amzn_sfx_boing_short_1x": "Boing Short 1x",
|
||||
"amzn_sfx_bus_drive_past": "Bus Drive Past",
|
||||
"amzn_sfx_buzz_electronic": "Buzz Electronic",
|
||||
"amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm",
|
||||
"amzn_sfx_buzzer_small": "Buzzer Small",
|
||||
"amzn_sfx_car_accelerate": "Car Accelerate",
|
||||
"amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy",
|
||||
"amzn_sfx_car_click_seatbelt": "Car Click Seatbelt",
|
||||
"amzn_sfx_car_close_door_1x": "Car Close Door 1x",
|
||||
"amzn_sfx_car_drive_past": "Car Drive Past",
|
||||
"amzn_sfx_car_honk_1x": "Car Honk 1x",
|
||||
"amzn_sfx_car_honk_2x": "Car Honk 2x",
|
||||
"amzn_sfx_car_honk_3x": "Car Honk 3x",
|
||||
"amzn_sfx_car_honk_long_1x": "Car Honk Long 1x",
|
||||
"amzn_sfx_car_into_driveway": "Car Into Driveway",
|
||||
"amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast",
|
||||
"amzn_sfx_car_slam_door_1x": "Car Slam Door 1x",
|
||||
"amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt",
|
||||
"amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x",
|
||||
"amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x",
|
||||
"amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x",
|
||||
"amzn_sfx_cat_meow_1x": "Cat Meow 1x",
|
||||
"amzn_sfx_cat_purr": "Cat Purr",
|
||||
"amzn_sfx_cat_purr_meow": "Cat Purr Meow",
|
||||
"amzn_sfx_chicken_cluck": "Chicken Cluck",
|
||||
"amzn_sfx_church_bell_1x": "Church Bell 1x",
|
||||
"amzn_sfx_church_bells_ringing": "Church Bells Ringing",
|
||||
"amzn_sfx_clear_throat_ahem": "Clear Throat Ahem",
|
||||
"amzn_sfx_clock_ticking": "Clock Ticking",
|
||||
"amzn_sfx_clock_ticking_long": "Clock Ticking Long",
|
||||
"amzn_sfx_copy_machine": "Copy Machine",
|
||||
"amzn_sfx_cough": "Cough",
|
||||
"amzn_sfx_crow_caw_1x": "Crow Caw 1x",
|
||||
"amzn_sfx_crowd_applause": "Crowd Applause",
|
||||
"amzn_sfx_crowd_bar": "Crowd Bar",
|
||||
"amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy",
|
||||
"amzn_sfx_crowd_boo": "Crowd Boo",
|
||||
"amzn_sfx_crowd_cheer_med": "Crowd Cheer Med",
|
||||
"amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer",
|
||||
"amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x",
|
||||
"amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x",
|
||||
"amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl",
|
||||
"amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x",
|
||||
"amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x",
|
||||
"amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x",
|
||||
"amzn_sfx_door_open": "Door Open",
|
||||
"amzn_sfx_door_shut": "Door Shut",
|
||||
"amzn_sfx_doorbell": "Doorbell",
|
||||
"amzn_sfx_doorbell_buzz": "Doorbell Buzz",
|
||||
"amzn_sfx_doorbell_chime": "Doorbell Chime",
|
||||
"amzn_sfx_drinking_slurp": "Drinking Slurp",
|
||||
"amzn_sfx_drum_and_cymbal": "Drum And Cymbal",
|
||||
"amzn_sfx_drum_comedy": "Drum Comedy",
|
||||
"amzn_sfx_earthquake_rumble": "Earthquake Rumble",
|
||||
"amzn_sfx_electric_guitar": "Electric Guitar",
|
||||
"amzn_sfx_electronic_beep": "Electronic Beep",
|
||||
"amzn_sfx_electronic_major_chord": "Electronic Major Chord",
|
||||
"amzn_sfx_elephant": "Elephant",
|
||||
"amzn_sfx_elevator_bell_1x": "Elevator Bell 1x",
|
||||
"amzn_sfx_elevator_open_bell": "Elevator Open Bell",
|
||||
"amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes",
|
||||
"amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes",
|
||||
"amzn_sfx_faucet_drip": "Faucet Drip",
|
||||
"amzn_sfx_faucet_running": "Faucet Running",
|
||||
"amzn_sfx_fireplace_crackle": "Fireplace Crackle",
|
||||
"amzn_sfx_fireworks": "Fireworks",
|
||||
"amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers",
|
||||
"amzn_sfx_fireworks_launch": "Fireworks Launch",
|
||||
"amzn_sfx_fireworks_whistles": "Fireworks Whistles",
|
||||
"amzn_sfx_food_frying": "Food Frying",
|
||||
"amzn_sfx_footsteps": "Footsteps",
|
||||
"amzn_sfx_footsteps_muffled": "Footsteps Muffled",
|
||||
"amzn_sfx_ghost_spooky": "Ghost Spooky",
|
||||
"amzn_sfx_glass_on_table": "Glass On Table",
|
||||
"amzn_sfx_glasses_clink": "Glasses Clink",
|
||||
"amzn_sfx_horse_gallop_4x": "Horse Gallop 4x",
|
||||
"amzn_sfx_horse_huff_whinny": "Horse Huff Whinny",
|
||||
"amzn_sfx_horse_neigh": "Horse Neigh",
|
||||
"amzn_sfx_horse_neigh_low": "Horse Neigh Low",
|
||||
"amzn_sfx_horse_whinny": "Horse Whinny",
|
||||
"amzn_sfx_human_walking": "Human Walking",
|
||||
"amzn_sfx_jar_on_table_1x": "Jar On Table 1x",
|
||||
"amzn_sfx_kitchen_ambience": "Kitchen Ambience",
|
||||
"amzn_sfx_large_crowd_cheer": "Large Crowd Cheer",
|
||||
"amzn_sfx_large_fire_crackling": "Large Fire Crackling",
|
||||
"amzn_sfx_laughter": "Laughter",
|
||||
"amzn_sfx_laughter_giggle": "Laughter Giggle",
|
||||
"amzn_sfx_lightning_strike": "Lightning Strike",
|
||||
"amzn_sfx_lion_roar": "Lion Roar",
|
||||
"amzn_sfx_magic_blast_1x": "Magic Blast 1x",
|
||||
"amzn_sfx_monkey_calls_3x": "Monkey Calls 3x",
|
||||
"amzn_sfx_monkey_chimp": "Monkey Chimp",
|
||||
"amzn_sfx_monkeys_chatter": "Monkeys Chatter",
|
||||
"amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate",
|
||||
"amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle",
|
||||
"amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev",
|
||||
"amzn_sfx_musical_drone_intro": "Musical Drone Intro",
|
||||
"amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat",
|
||||
"amzn_sfx_object_on_table_2x": "Object On Table 2x",
|
||||
"amzn_sfx_ocean_wave_1x": "Ocean Wave 1x",
|
||||
"amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x",
|
||||
"amzn_sfx_ocean_wave_surf": "Ocean Wave Surf",
|
||||
"amzn_sfx_people_walking": "People Walking",
|
||||
"amzn_sfx_person_running": "Person Running",
|
||||
"amzn_sfx_piano_note_1x": "Piano Note 1x",
|
||||
"amzn_sfx_punch": "Punch",
|
||||
"amzn_sfx_rain": "Rain",
|
||||
"amzn_sfx_rain_on_roof": "Rain On Roof",
|
||||
"amzn_sfx_rain_thunder": "Rain Thunder",
|
||||
"amzn_sfx_rat_squeak_2x": "Rat Squeak 2x",
|
||||
"amzn_sfx_rat_squeaks": "Rat Squeaks",
|
||||
"amzn_sfx_raven_caw_1x": "Raven Caw 1x",
|
||||
"amzn_sfx_raven_caw_2x": "Raven Caw 2x",
|
||||
"amzn_sfx_restaurant_ambience": "Restaurant Ambience",
|
||||
"amzn_sfx_rooster_crow": "Rooster Crow",
|
||||
"amzn_sfx_scifi_air_escaping": "Scifi Air Escaping",
|
||||
"amzn_sfx_scifi_alarm": "Scifi Alarm",
|
||||
"amzn_sfx_scifi_alien_voice": "Scifi Alien Voice",
|
||||
"amzn_sfx_scifi_boots_walking": "Scifi Boots Walking",
|
||||
"amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion",
|
||||
"amzn_sfx_scifi_door_open": "Scifi Door Open",
|
||||
"amzn_sfx_scifi_engines_on": "Scifi Engines On",
|
||||
"amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large",
|
||||
"amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst",
|
||||
"amzn_sfx_scifi_explosion": "Scifi Explosion",
|
||||
"amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x",
|
||||
"amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion",
|
||||
"amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle",
|
||||
"amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires",
|
||||
"amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large",
|
||||
"amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x",
|
||||
"amzn_sfx_scifi_missile": "Scifi Missile",
|
||||
"amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x",
|
||||
"amzn_sfx_scifi_open_airlock": "Scifi Open Airlock",
|
||||
"amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping",
|
||||
"amzn_sfx_scifi_radar_low": "Scifi Radar Low",
|
||||
"amzn_sfx_scifi_radar_medium": "Scifi Radar Medium",
|
||||
"amzn_sfx_scifi_run_away": "Scifi Run Away",
|
||||
"amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up",
|
||||
"amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion",
|
||||
"amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby",
|
||||
"amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby",
|
||||
"amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x",
|
||||
"amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x",
|
||||
"amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby",
|
||||
"amzn_sfx_scifi_timer_beep": "Scifi Timer Beep",
|
||||
"amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards",
|
||||
"amzn_sfx_scifi_zap_electric": "Scifi Zap Electric",
|
||||
"amzn_sfx_sheep_baa": "Sheep Baa",
|
||||
"amzn_sfx_sheep_bleat": "Sheep Bleat",
|
||||
"amzn_sfx_silverware_clank": "Silverware Clank",
|
||||
"amzn_sfx_sirens": "Sirens",
|
||||
"amzn_sfx_sleigh_bells": "Sleigh Bells",
|
||||
"amzn_sfx_small_stream": "Small Stream",
|
||||
"amzn_sfx_sneeze": "Sneeze",
|
||||
"amzn_sfx_stream": "Stream",
|
||||
"amzn_sfx_strong_wind_desert": "Strong Wind Desert",
|
||||
"amzn_sfx_strong_wind_whistling": "Strong Wind Whistling",
|
||||
"amzn_sfx_subway_leaving": "Subway Leaving",
|
||||
"amzn_sfx_subway_passing": "Subway Passing",
|
||||
"amzn_sfx_subway_stopping": "Subway Stopping",
|
||||
"amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast",
|
||||
"amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x",
|
||||
"amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x",
|
||||
"amzn_sfx_test_tone": "Test Tone",
|
||||
"amzn_sfx_thunder_rumble": "Thunder Rumble",
|
||||
"amzn_sfx_toilet_flush": "Toilet Flush",
|
||||
"amzn_sfx_trumpet_bugle": "Trumpet Bugle",
|
||||
"amzn_sfx_turkey_gobbling": "Turkey Gobbling",
|
||||
"amzn_sfx_typing_medium": "Typing Medium",
|
||||
"amzn_sfx_typing_short": "Typing Short",
|
||||
"amzn_sfx_typing_typewriter": "Typing Typewriter",
|
||||
"amzn_sfx_vacuum_off": "Vacuum Off",
|
||||
"amzn_sfx_vacuum_on": "Vacuum On",
|
||||
"amzn_sfx_walking_in_mud": "Walking In Mud",
|
||||
"amzn_sfx_walking_in_snow": "Walking In Snow",
|
||||
"amzn_sfx_walking_on_grass": "Walking On Grass",
|
||||
"amzn_sfx_water_dripping": "Water Dripping",
|
||||
"amzn_sfx_water_droplets": "Water Droplets",
|
||||
"amzn_sfx_wind_strong_gusting": "Wind Strong Gusting",
|
||||
"amzn_sfx_wind_whistling_desert": "Wind Whistling Desert",
|
||||
"amzn_sfx_wings_flap_4x": "Wings Flap 4x",
|
||||
"amzn_sfx_wings_flap_fast": "Wings Flap Fast",
|
||||
"amzn_sfx_wolf_howl": "Wolf Howl",
|
||||
"amzn_sfx_wolf_young_howl": "Wolf Young Howl",
|
||||
"amzn_sfx_wooden_door": "Wooden Door",
|
||||
"amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long",
|
||||
"amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple",
|
||||
"amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open",
|
||||
"amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal",
|
||||
"amzn_ui_sfx_gameshow_intro": "Gameshow Intro",
|
||||
"amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response",
|
||||
"amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response",
|
||||
"amzn_ui_sfx_gameshow_outro": "Gameshow Outro",
|
||||
"amzn_ui_sfx_gameshow_player1": "Gameshow Player1",
|
||||
"amzn_ui_sfx_gameshow_player2": "Gameshow Player2",
|
||||
"amzn_ui_sfx_gameshow_player3": "Gameshow Player3",
|
||||
"amzn_ui_sfx_gameshow_player4": "Gameshow Player4",
|
||||
"amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response",
|
||||
"amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative",
|
||||
"amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive",
|
||||
"amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s",
|
||||
"anchor": "Anchor",
|
||||
"answering_machines": "Answering Machines",
|
||||
"arcs_sparks": "Arcs Sparks",
|
||||
"arrows_bows": "Arrows Bows",
|
||||
"baby": "Baby",
|
||||
"back_up_beeps": "Back Up Beeps",
|
||||
"bars_restaurants": "Bars Restaurants",
|
||||
"baseball": "Baseball",
|
||||
"basketball": "Basketball",
|
||||
"battles": "Battles",
|
||||
"beeps_tones": "Beeps Tones",
|
||||
"bell": "Bell",
|
||||
"bikes": "Bikes",
|
||||
"billiards": "Billiards",
|
||||
"board_games": "Board Games",
|
||||
"body": "Body",
|
||||
"boing": "Boing",
|
||||
"books": "Books",
|
||||
"bow_wash": "Bow Wash",
|
||||
"box": "Box",
|
||||
"break_shatter_smash": "Break Shatter Smash",
|
||||
"breaks": "Breaks",
|
||||
"brooms_mops": "Brooms Mops",
|
||||
"bullets": "Bullets",
|
||||
"buses": "Buses",
|
||||
"buzz": "Buzz",
|
||||
"buzz_hums": "Buzz Hums",
|
||||
"buzzers": "Buzzers",
|
||||
"buzzers_pistols": "Buzzers Pistols",
|
||||
"cables_metal": "Cables Metal",
|
||||
"camera": "Camera",
|
||||
"cannons": "Cannons",
|
||||
"car_alarm": "Car Alarm",
|
||||
"car_alarms": "Car Alarms",
|
||||
"car_cell_phones": "Car Cell Phones",
|
||||
"carnivals_fairs": "Carnivals Fairs",
|
||||
"cars": "Cars",
|
||||
"casino": "Casino",
|
||||
"casinos": "Casinos",
|
||||
"cellar": "Cellar",
|
||||
"chimes": "Chimes",
|
||||
"chimes_bells": "Chimes Bells",
|
||||
"chorus": "Chorus",
|
||||
"christmas": "Christmas",
|
||||
"church_bells": "Church Bells",
|
||||
"clock": "Clock",
|
||||
"cloth": "Cloth",
|
||||
"concrete": "Concrete",
|
||||
"construction": "Construction",
|
||||
"construction_factory": "Construction Factory",
|
||||
"crashes": "Crashes",
|
||||
"crowds": "Crowds",
|
||||
"debris": "Debris",
|
||||
"dining_kitchens": "Dining Kitchens",
|
||||
"dinosaurs": "Dinosaurs",
|
||||
"dripping": "Dripping",
|
||||
"drops": "Drops",
|
||||
"electric": "Electric",
|
||||
"electrical": "Electrical",
|
||||
"elevator": "Elevator",
|
||||
"evolution_monsters": "Evolution Monsters",
|
||||
"explosions": "Explosions",
|
||||
"factory": "Factory",
|
||||
"falls": "Falls",
|
||||
"fax_scanner_copier": "Fax Scanner Copier",
|
||||
"feedback_mics": "Feedback Mics",
|
||||
"fight": "Fight",
|
||||
"fire": "Fire",
|
||||
"fire_extinguisher": "Fire Extinguisher",
|
||||
"fireballs": "Fireballs",
|
||||
"fireworks": "Fireworks",
|
||||
"fishing_pole": "Fishing Pole",
|
||||
"flags": "Flags",
|
||||
"football": "Football",
|
||||
"footsteps": "Footsteps",
|
||||
"futuristic": "Futuristic",
|
||||
"futuristic_ship": "Futuristic Ship",
|
||||
"gameshow": "Gameshow",
|
||||
"gear": "Gear",
|
||||
"ghosts_demons": "Ghosts Demons",
|
||||
"giant_monster": "Giant Monster",
|
||||
"glass": "Glass",
|
||||
"glasses_clink": "Glasses Clink",
|
||||
"golf": "Golf",
|
||||
"gorilla": "Gorilla",
|
||||
"grenade_lanucher": "Grenade Lanucher",
|
||||
"griffen": "Griffen",
|
||||
"gyms_locker_rooms": "Gyms Locker Rooms",
|
||||
"handgun_loading": "Handgun Loading",
|
||||
"handgun_shot": "Handgun Shot",
|
||||
"handle": "Handle",
|
||||
"hands": "Hands",
|
||||
"heartbeats_ekg": "Heartbeats EKG",
|
||||
"helicopter": "Helicopter",
|
||||
"high_tech": "High Tech",
|
||||
"hit_punch_slap": "Hit Punch Slap",
|
||||
"hits": "Hits",
|
||||
"horns": "Horns",
|
||||
"horror": "Horror",
|
||||
"hot_tub_filling_up": "Hot Tub Filling Up",
|
||||
"human": "Human",
|
||||
"human_vocals": "Human Vocals",
|
||||
"hygene": "Hygene",
|
||||
"ice_skating": "Ice Skating",
|
||||
"ignitions": "Ignitions",
|
||||
"infantry": "Infantry",
|
||||
"intro": "Intro",
|
||||
"jet": "Jet",
|
||||
"juggling": "Juggling",
|
||||
"key_lock": "Key Lock",
|
||||
"kids": "Kids",
|
||||
"knocks": "Knocks",
|
||||
"lab_equip": "Lab Equip",
|
||||
"lacrosse": "Lacrosse",
|
||||
"lamps_lanterns": "Lamps Lanterns",
|
||||
"leather": "Leather",
|
||||
"liquid_suction": "Liquid Suction",
|
||||
"locker_doors": "Locker Doors",
|
||||
"machine_gun": "Machine Gun",
|
||||
"magic_spells": "Magic Spells",
|
||||
"medium_large_explosions": "Medium Large Explosions",
|
||||
"metal": "Metal",
|
||||
"modern_rings": "Modern Rings",
|
||||
"money_coins": "Money Coins",
|
||||
"motorcycles": "Motorcycles",
|
||||
"movement": "Movement",
|
||||
"moves": "Moves",
|
||||
"nature": "Nature",
|
||||
"oar_boat": "Oar Boat",
|
||||
"pagers": "Pagers",
|
||||
"paintball": "Paintball",
|
||||
"paper": "Paper",
|
||||
"parachute": "Parachute",
|
||||
"pay_phones": "Pay Phones",
|
||||
"phone_beeps": "Phone Beeps",
|
||||
"pigmy_bats": "Pigmy Bats",
|
||||
"pills": "Pills",
|
||||
"pour_water": "Pour Water",
|
||||
"power_up_down": "Power Up Down",
|
||||
"printers": "Printers",
|
||||
"prison": "Prison",
|
||||
"public_space": "Public Space",
|
||||
"racquetball": "Racquetball",
|
||||
"radios_static": "Radios Static",
|
||||
"rain": "Rain",
|
||||
"rc_airplane": "RC Airplane",
|
||||
"rc_car": "RC Car",
|
||||
"refrigerators_freezers": "Refrigerators Freezers",
|
||||
"regular": "Regular",
|
||||
"respirator": "Respirator",
|
||||
"rifle": "Rifle",
|
||||
"roller_coaster": "Roller Coaster",
|
||||
"rollerskates_rollerblades": "RollerSkates RollerBlades",
|
||||
"room_tones": "Room Tones",
|
||||
"ropes_climbing": "Ropes Climbing",
|
||||
"rotary_rings": "Rotary Rings",
|
||||
"rowboat_canoe": "Rowboat Canoe",
|
||||
"rubber": "Rubber",
|
||||
"running": "Running",
|
||||
"sails": "Sails",
|
||||
"sand_gravel": "Sand Gravel",
|
||||
"screen_doors": "Screen Doors",
|
||||
"screens": "Screens",
|
||||
"seats_stools": "Seats Stools",
|
||||
"servos": "Servos",
|
||||
"shoes_boots": "Shoes Boots",
|
||||
"shotgun": "Shotgun",
|
||||
"shower": "Shower",
|
||||
"sink_faucet": "Sink Faucet",
|
||||
"sink_filling_water": "Sink Filling Water",
|
||||
"sink_run_and_off": "Sink Run And Off",
|
||||
"sink_water_splatter": "Sink Water Splatter",
|
||||
"sirens": "Sirens",
|
||||
"skateboards": "Skateboards",
|
||||
"ski": "Ski",
|
||||
"skids_tires": "Skids Tires",
|
||||
"sled": "Sled",
|
||||
"slides": "Slides",
|
||||
"small_explosions": "Small Explosions",
|
||||
"snow": "Snow",
|
||||
"snowmobile": "Snowmobile",
|
||||
"soldiers": "Soldiers",
|
||||
"splash_water": "Splash Water",
|
||||
"splashes_sprays": "Splashes Sprays",
|
||||
"sports_whistles": "Sports Whistles",
|
||||
"squeaks": "Squeaks",
|
||||
"squeaky": "Squeaky",
|
||||
"stairs": "Stairs",
|
||||
"steam": "Steam",
|
||||
"submarine_diesel": "Submarine Diesel",
|
||||
"swing_doors": "Swing Doors",
|
||||
"switches_levers": "Switches Levers",
|
||||
"swords": "Swords",
|
||||
"tape": "Tape",
|
||||
"tape_machine": "Tape Machine",
|
||||
"televisions_shows": "Televisions Shows",
|
||||
"tennis_pingpong": "Tennis PingPong",
|
||||
"textile": "Textile",
|
||||
"throw": "Throw",
|
||||
"thunder": "Thunder",
|
||||
"ticks": "Ticks",
|
||||
"timer": "Timer",
|
||||
"toilet_flush": "Toilet Flush",
|
||||
"tone": "Tone",
|
||||
"tones_noises": "Tones Noises",
|
||||
"toys": "Toys",
|
||||
"tractors": "Tractors",
|
||||
"traffic": "Traffic",
|
||||
"train": "Train",
|
||||
"trucks_vans": "Trucks Vans",
|
||||
"turnstiles": "Turnstiles",
|
||||
"typing": "Typing",
|
||||
"umbrella": "Umbrella",
|
||||
"underwater": "Underwater",
|
||||
"vampires": "Vampires",
|
||||
"various": "Various",
|
||||
"video_tunes": "Video Tunes",
|
||||
"volcano_earthquake": "Volcano Earthquake",
|
||||
"watches": "Watches",
|
||||
"water": "Water",
|
||||
"water_running": "Water Running",
|
||||
"werewolves": "Werewolves",
|
||||
"winches_gears": "Winches Gears",
|
||||
"wind": "Wind",
|
||||
"wood": "Wood",
|
||||
"wood_boat": "Wood Boat",
|
||||
"woosh": "Woosh",
|
||||
"zap": "Zap",
|
||||
"zippers": "Zippers"
|
||||
"air_horn_03": "Air horn",
|
||||
"amzn_sfx_cat_meow_1x_01": "Cat meow",
|
||||
"amzn_sfx_church_bell_1x_02": "Church bell",
|
||||
"amzn_sfx_crowd_applause_01": "Crowd applause",
|
||||
"amzn_sfx_dog_med_bark_1x_02": "Dog bark",
|
||||
"amzn_sfx_doorbell_01": "Doorbell 1",
|
||||
"amzn_sfx_doorbell_chime_01": "Doorbell 2",
|
||||
"amzn_sfx_doorbell_chime_02": "Doorbell 3",
|
||||
"amzn_sfx_large_crowd_cheer_01": "Crowd cheers",
|
||||
"amzn_sfx_lion_roar_02": "Lion roar",
|
||||
"amzn_sfx_rooster_crow_01": "Rooster",
|
||||
"amzn_sfx_scifi_alarm_01": "Sirens",
|
||||
"amzn_sfx_scifi_alarm_04": "Red alert",
|
||||
"amzn_sfx_scifi_engines_on_02": "Engines on",
|
||||
"amzn_sfx_scifi_sheilds_up_01": "Shields up",
|
||||
"amzn_sfx_trumpet_bugle_04": "Trumpet",
|
||||
"amzn_sfx_wolf_howl_02": "Wolf howl",
|
||||
"bell_02": "Bells",
|
||||
"boing_01": "Boing 1",
|
||||
"boing_03": "Boing 2",
|
||||
"buzzers_pistols_01": "Buzzer",
|
||||
"camera_01": "Camera",
|
||||
"christmas_05": "Christmas bells",
|
||||
"clock_01": "Ticking clock",
|
||||
"futuristic_10": "Aircraft",
|
||||
"halloween_bats": "Halloween bats",
|
||||
"halloween_crows": "Halloween crows",
|
||||
"halloween_footsteps": "Halloween spooky footsteps",
|
||||
"halloween_wind": "Halloween wind",
|
||||
"halloween_wolf": "Halloween wolf",
|
||||
"holiday_halloween_ghost": "Halloween ghost",
|
||||
"horror_10": "Halloween creepy door",
|
||||
"med_system_alerts_minimal_dragon_short": "Friendly dragon",
|
||||
"med_system_alerts_minimal_owl_short": "Happy owl",
|
||||
"med_system_alerts_minimals_blue_wave_small": "Underwater World Sonata",
|
||||
"med_system_alerts_minimals_galaxy_short": "Infinite Galaxy",
|
||||
"med_system_alerts_minimals_panda_short": "Baby panda",
|
||||
"med_system_alerts_minimals_tiger_short": "Playful tiger",
|
||||
"med_ui_success_generic_1-1": "Success 1",
|
||||
"squeaky_12": "Squeaky door",
|
||||
"zap_01": "Zap"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -607,7 +187,7 @@
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
"invalid_sound_value": {
|
||||
"message": "Invalid sound {sound} with variant {variant} specified"
|
||||
"message": "Invalid sound {sound} specified"
|
||||
},
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api = amberelectric.AmberApi(api_client)
|
||||
|
||||
try:
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
sites: list[Site] = filter_sites(
|
||||
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
|
||||
)
|
||||
except amberelectric.ApiException as api_exception:
|
||||
if api_exception.status == 403:
|
||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||
|
||||
@@ -9,7 +9,6 @@ DOMAIN: Final = "amberelectric"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_CHANNEL_TYPE = "channel_type"
|
||||
|
||||
ATTRIBUTION = "Data provided by Amber Electric"
|
||||
@@ -22,3 +21,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
|
||||
GENERAL_CHANNEL = "general"
|
||||
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||
FEED_IN_CHANNEL = "feed_in"
|
||||
|
||||
REQUEST_TIMEOUT = 15
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import LOGGER, REQUEST_TIMEOUT
|
||||
from .helpers import normalize_descriptor
|
||||
|
||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
"grid": {},
|
||||
}
|
||||
try:
|
||||
data = self._api.get_current_prices(self.site_id, next=288)
|
||||
data = self._api.get_current_prices(
|
||||
self.site_id,
|
||||
next=288,
|
||||
_request_timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
intervals = [interval.actual_instance for interval in data]
|
||||
except ApiException as api_exception:
|
||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||
|
||||
@@ -4,6 +4,7 @@ from amberelectric.models.channel import ChannelType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
@@ -16,7 +17,6 @@ from homeassistant.util.json import JsonValueType
|
||||
|
||||
from .const import (
|
||||
ATTR_CHANNEL_TYPE,
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONTROLLED_LOAD_CHANNEL,
|
||||
DOMAIN,
|
||||
FEED_IN_CHANNEL,
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from aioambient.util import get_public_device_id
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
@@ -37,6 +37,7 @@ class AmbientWeatherEntity(Entity):
|
||||
identifiers={(DOMAIN, mac_address)},
|
||||
manufacturer="Ambient Weather",
|
||||
name=station_name.capitalize(),
|
||||
connections={(CONNECTION_NETWORK_MAC, mac_address)},
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{mac_address}_{description.key}"
|
||||
|
||||
@@ -390,7 +390,6 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
|
||||
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"""Return the devices payload."""
|
||||
integrations_without_model_id: set[str] = set()
|
||||
devices: list[dict[str, Any]] = []
|
||||
dev_reg = dr.async_get(hass)
|
||||
# Devices that need via device info set
|
||||
@@ -400,10 +399,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
seen_integrations = set()
|
||||
|
||||
for device in dev_reg.devices.values():
|
||||
# Ignore services
|
||||
if device.entry_type:
|
||||
continue
|
||||
|
||||
if not device.primary_config_entry:
|
||||
continue
|
||||
|
||||
@@ -414,13 +409,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
|
||||
seen_integrations.add(config_entry.domain)
|
||||
|
||||
if not device.model_id:
|
||||
integrations_without_model_id.add(config_entry.domain)
|
||||
continue
|
||||
|
||||
if not device.manufacturer:
|
||||
continue
|
||||
|
||||
new_indexes[device.id] = len(devices)
|
||||
devices.append(
|
||||
{
|
||||
@@ -432,8 +420,10 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"hw_version": device.hw_version,
|
||||
"has_configuration_url": device.configuration_url is not None,
|
||||
"via_device": None,
|
||||
"entry_type": device.entry_type.value if device.entry_type else None,
|
||||
}
|
||||
)
|
||||
|
||||
if device.via_device_id:
|
||||
via_devices[device.id] = device.via_device_id
|
||||
|
||||
@@ -453,15 +443,12 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
for device_info in devices:
|
||||
if integration := integrations.get(device_info["integration"]):
|
||||
device_info["is_custom_integration"] = not integration.is_built_in
|
||||
# Include version for custom integrations
|
||||
if not integration.is_built_in and integration.version:
|
||||
device_info["custom_integration_version"] = str(integration.version)
|
||||
|
||||
return {
|
||||
"version": "home-assistant:1",
|
||||
"no_model_id": sorted(
|
||||
[
|
||||
domain
|
||||
for domain in integrations_without_model_id
|
||||
if domain in integrations and integrations[domain].is_built_in
|
||||
]
|
||||
),
|
||||
"home_assistant": HA_VERSION,
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -30,10 +30,9 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
cam: PyDroidIPCam,
|
||||
) -> None:
|
||||
"""Initialize the Android IP Webcam."""
|
||||
self.hass = hass
|
||||
self.cam = cam
|
||||
super().__init__(
|
||||
self.hass,
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN} {config_entry.data[CONF_HOST]}",
|
||||
|
||||
@@ -81,11 +81,15 @@ async def async_update_options(
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
# Make sure we get enabled config entries first
|
||||
entries = sorted(
|
||||
hass.config_entries.async_entries(DOMAIN),
|
||||
key=lambda e: e.disabled_by is not None,
|
||||
)
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
)
|
||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||
all_disabled = all(
|
||||
e.disabled_by is not None
|
||||
for e in entries
|
||||
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||
)
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
|
||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||
conversation_entity = entity_registry.async_get_entity_id(
|
||||
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||
"conversation",
|
||||
DOMAIN,
|
||||
entry.entry_id,
|
||||
)
|
||||
if conversation_entity is not None:
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, entry.entry_id)}
|
||||
)
|
||||
|
||||
if conversation_entity_id is not None:
|
||||
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||
if (
|
||||
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
# Device and entity registries will set the disabled_by flag to None
|
||||
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
|
||||
# config entry, but we want to set it to DEVICE or USER instead,
|
||||
entity_disabled_by = (
|
||||
er.RegistryEntryDisabler.DEVICE
|
||||
if device
|
||||
else er.RegistryEntryDisabler.USER
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity_id,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
disabled_by=entity_disabled_by,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
if device is not None:
|
||||
# Device and entity registries will set the disabled_by flag to None
|
||||
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
|
||||
# config entry, but we want to set it to USER instead,
|
||||
device_disabled_by = device.disabled_by
|
||||
if (
|
||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=device_disabled_by,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=DEFAULT_CONVERSATION_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
|
||||
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
# Fix migration where the disabled_by flag was not set correctly.
|
||||
# We can currently only correct this for enabled config entries,
|
||||
# because migration does not run for disabled config entries. This
|
||||
# is asserted in tests, and if that behavior is changed, we should
|
||||
# correct also disabled config entries.
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
if entry.disabled_by is None:
|
||||
# If the config entry is not disabled, we need to set the disabled_by
|
||||
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||
# to CONFIG_ENTRY.
|
||||
for device in devices:
|
||||
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||
)
|
||||
for entity in entity_entries:
|
||||
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
entity_registry.async_update_entity(
|
||||
entity.entity_id,
|
||||
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -20,10 +20,8 @@ RECOMMENDED_THINKING_BUDGET = 0
|
||||
MIN_THINKING_BUDGET = 1024
|
||||
|
||||
THINKING_MODELS = [
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-3-7-sonnet-latest",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-3-7-sonnet",
|
||||
"claude-sonnet-4-0",
|
||||
"claude-opus-4-0",
|
||||
"claude-opus-4-1",
|
||||
]
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from collections.abc import AsyncGenerator, Callable, Iterable
|
||||
import json
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import anthropic
|
||||
from anthropic import AsyncStream
|
||||
from anthropic._types import NOT_GIVEN
|
||||
from anthropic.types import (
|
||||
InputJSONDelta,
|
||||
MessageDeltaUsage,
|
||||
@@ -17,7 +16,6 @@ from anthropic.types import (
|
||||
RawContentBlockStopEvent,
|
||||
RawMessageDeltaEvent,
|
||||
RawMessageStartEvent,
|
||||
RawMessageStopEvent,
|
||||
RedactedThinkingBlock,
|
||||
RedactedThinkingBlockParam,
|
||||
SignatureDelta,
|
||||
@@ -35,6 +33,7 @@ from anthropic.types import (
|
||||
ToolUseBlockParam,
|
||||
Usage,
|
||||
)
|
||||
from anthropic.types.message_create_params import MessageCreateParamsStreaming
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
from homeassistant.components import conversation
|
||||
@@ -129,6 +128,28 @@ def _convert_content(
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(content.native, ThinkingBlock):
|
||||
messages[-1]["content"].append( # type: ignore[union-attr]
|
||||
ThinkingBlockParam(
|
||||
type="thinking",
|
||||
thinking=content.thinking_content or "",
|
||||
signature=content.native.signature,
|
||||
)
|
||||
)
|
||||
elif isinstance(content.native, RedactedThinkingBlock):
|
||||
redacted_thinking_block = RedactedThinkingBlockParam(
|
||||
type="redacted_thinking",
|
||||
data=content.native.data,
|
||||
)
|
||||
if isinstance(messages[-1]["content"], str):
|
||||
messages[-1]["content"] = [
|
||||
TextBlockParam(type="text", text=messages[-1]["content"]),
|
||||
redacted_thinking_block,
|
||||
]
|
||||
else:
|
||||
messages[-1]["content"].append( # type: ignore[attr-defined]
|
||||
redacted_thinking_block
|
||||
)
|
||||
if content.content:
|
||||
messages[-1]["content"].append( # type: ignore[union-attr]
|
||||
TextBlockParam(type="text", text=content.content)
|
||||
@@ -152,10 +173,9 @@ def _convert_content(
|
||||
return messages
|
||||
|
||||
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
async def _transform_stream(
|
||||
chat_log: conversation.ChatLog,
|
||||
result: AsyncStream[MessageStreamEvent],
|
||||
messages: list[MessageParam],
|
||||
stream: AsyncStream[MessageStreamEvent],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
"""Transform the response stream into HA format.
|
||||
|
||||
@@ -186,31 +206,25 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
|
||||
Each message could contain multiple blocks of the same type.
|
||||
"""
|
||||
if result is None:
|
||||
if stream is None:
|
||||
raise TypeError("Expected a stream of messages")
|
||||
|
||||
current_message: MessageParam | None = None
|
||||
current_block: (
|
||||
TextBlockParam
|
||||
| ToolUseBlockParam
|
||||
| ThinkingBlockParam
|
||||
| RedactedThinkingBlockParam
|
||||
| None
|
||||
) = None
|
||||
current_tool_block: ToolUseBlockParam | None = None
|
||||
current_tool_args: str
|
||||
input_usage: Usage | None = None
|
||||
has_content = False
|
||||
has_native = False
|
||||
|
||||
async for response in result:
|
||||
async for response in stream:
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
|
||||
if isinstance(response, RawMessageStartEvent):
|
||||
if response.message.role != "assistant":
|
||||
raise ValueError("Unexpected message role")
|
||||
current_message = MessageParam(role=response.message.role, content=[])
|
||||
input_usage = response.message.usage
|
||||
elif isinstance(response, RawContentBlockStartEvent):
|
||||
if isinstance(response.content_block, ToolUseBlock):
|
||||
current_block = ToolUseBlockParam(
|
||||
current_tool_block = ToolUseBlockParam(
|
||||
type="tool_use",
|
||||
id=response.content_block.id,
|
||||
name=response.content_block.name,
|
||||
@@ -218,75 +232,64 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
)
|
||||
current_tool_args = ""
|
||||
elif isinstance(response.content_block, TextBlock):
|
||||
current_block = TextBlockParam(
|
||||
type="text", text=response.content_block.text
|
||||
)
|
||||
yield {"role": "assistant"}
|
||||
if has_content:
|
||||
yield {"role": "assistant"}
|
||||
has_native = False
|
||||
has_content = True
|
||||
if response.content_block.text:
|
||||
yield {"content": response.content_block.text}
|
||||
elif isinstance(response.content_block, ThinkingBlock):
|
||||
current_block = ThinkingBlockParam(
|
||||
type="thinking",
|
||||
thinking=response.content_block.thinking,
|
||||
signature=response.content_block.signature,
|
||||
)
|
||||
if has_native:
|
||||
yield {"role": "assistant"}
|
||||
has_native = False
|
||||
has_content = False
|
||||
elif isinstance(response.content_block, RedactedThinkingBlock):
|
||||
current_block = RedactedThinkingBlockParam(
|
||||
type="redacted_thinking", data=response.content_block.data
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Some of Claude’s internal reasoning has been automatically "
|
||||
"encrypted for safety reasons. This doesn’t affect the quality of "
|
||||
"responses"
|
||||
)
|
||||
if has_native:
|
||||
yield {"role": "assistant"}
|
||||
has_native = False
|
||||
has_content = False
|
||||
yield {"native": response.content_block}
|
||||
has_native = True
|
||||
elif isinstance(response, RawContentBlockDeltaEvent):
|
||||
if current_block is None:
|
||||
raise ValueError("Unexpected delta without a block")
|
||||
if isinstance(response.delta, InputJSONDelta):
|
||||
current_tool_args += response.delta.partial_json
|
||||
elif isinstance(response.delta, TextDelta):
|
||||
text_block = cast(TextBlockParam, current_block)
|
||||
text_block["text"] += response.delta.text
|
||||
yield {"content": response.delta.text}
|
||||
elif isinstance(response.delta, ThinkingDelta):
|
||||
thinking_block = cast(ThinkingBlockParam, current_block)
|
||||
thinking_block["thinking"] += response.delta.thinking
|
||||
yield {"thinking_content": response.delta.thinking}
|
||||
elif isinstance(response.delta, SignatureDelta):
|
||||
thinking_block = cast(ThinkingBlockParam, current_block)
|
||||
thinking_block["signature"] += response.delta.signature
|
||||
yield {
|
||||
"native": ThinkingBlock(
|
||||
type="thinking",
|
||||
thinking="",
|
||||
signature=response.delta.signature,
|
||||
)
|
||||
}
|
||||
has_native = True
|
||||
elif isinstance(response, RawContentBlockStopEvent):
|
||||
if current_block is None:
|
||||
raise ValueError("Unexpected stop event without a current block")
|
||||
if current_block["type"] == "tool_use":
|
||||
# tool block
|
||||
if current_tool_block is not None:
|
||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||
current_block["input"] = tool_args
|
||||
current_tool_block["input"] = tool_args
|
||||
yield {
|
||||
"tool_calls": [
|
||||
llm.ToolInput(
|
||||
id=current_block["id"],
|
||||
tool_name=current_block["name"],
|
||||
id=current_tool_block["id"],
|
||||
tool_name=current_tool_block["name"],
|
||||
tool_args=tool_args,
|
||||
)
|
||||
]
|
||||
}
|
||||
elif current_block["type"] == "thinking":
|
||||
# thinking block
|
||||
LOGGER.debug("Thinking: %s", current_block["thinking"])
|
||||
|
||||
if current_message is None:
|
||||
raise ValueError("Unexpected stop event without a current message")
|
||||
current_message["content"].append(current_block) # type: ignore[union-attr]
|
||||
current_block = None
|
||||
current_tool_block = None
|
||||
elif isinstance(response, RawMessageDeltaEvent):
|
||||
if (usage := response.usage) is not None:
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
if response.delta.stop_reason == "refusal":
|
||||
raise HomeAssistantError("Potential policy violation detected")
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if current_message is not None:
|
||||
messages.append(current_message)
|
||||
current_message = None
|
||||
|
||||
|
||||
def _create_token_stats(
|
||||
@@ -351,45 +354,48 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
|
||||
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
|
||||
model_args = MessageCreateParamsStreaming(
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
system=system.content,
|
||||
stream=True,
|
||||
)
|
||||
if tools:
|
||||
model_args["tools"] = tools
|
||||
if (
|
||||
model.startswith(tuple(THINKING_MODELS))
|
||||
and thinking_budget >= MIN_THINKING_BUDGET
|
||||
):
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
)
|
||||
|
||||
# To prevent infinite loops, we limit the number of iterations
|
||||
for _iteration in range(MAX_TOOL_ITERATIONS):
|
||||
model_args = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"tools": tools or NOT_GIVEN,
|
||||
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
"system": system.content,
|
||||
"stream": True,
|
||||
}
|
||||
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
|
||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||
type="enabled", budget_tokens=thinking_budget
|
||||
)
|
||||
else:
|
||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||
model_args["temperature"] = options.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
)
|
||||
|
||||
try:
|
||||
stream = await client.messages.create(**model_args)
|
||||
|
||||
messages.extend(
|
||||
_convert_content(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(chat_log, stream),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
except anthropic.AnthropicError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Sorry, I had a problem talking to Anthropic: {err}"
|
||||
) from err
|
||||
|
||||
messages.extend(
|
||||
_convert_content(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(chat_log, stream, messages),
|
||||
)
|
||||
if not isinstance(content, conversation.AssistantContent)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if not chat_log.unresponded_tool_results:
|
||||
break
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.52.0"]
|
||||
"requirements": ["anthropic==0.62.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
|
||||
PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR)
|
||||
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -10,9 +10,9 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -40,22 +40,16 @@ async def async_setup_entry(
|
||||
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
|
||||
|
||||
|
||||
class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
|
||||
class OnlineStatus(APCUPSdEntity, BinarySensorEntity):
|
||||
"""Representation of a UPS online status."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: BinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the APCUPSd binary device."""
|
||||
super().__init__(coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -100,6 +100,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
||||
name=self.data.name or "APC UPS",
|
||||
hw_version=self.data.get("FIRMWARE"),
|
||||
sw_version=self.data.get("VERSION"),
|
||||
serial_number=self.data.serial_no,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> APCUPSdData:
|
||||
|
||||
26
homeassistant/components/apcupsd/entity.py
Normal file
26
homeassistant/components/apcupsd/entity.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Base entity for APCUPSd integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
|
||||
class APCUPSdEntity(CoordinatorEntity[APCUPSdCoordinator]):
|
||||
"""Base entity for APCUPSd integration."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the APCUPSd entity."""
|
||||
super().__init__(coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["apcaccess"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioapcaccess==0.4.2"]
|
||||
}
|
||||
|
||||
90
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
90
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
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: |
|
||||
The integration does not provide any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional options.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not require authentication.
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
Patch `aioapcaccess.request_status` where we use it.
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration connects to a single service per configuration entry.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration connect to a single service per configuration entry.
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not connect via HTTP.
|
||||
strict-typing: done
|
||||
@@ -23,10 +23,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import LAST_S_TEST
|
||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||
from .entity import APCUPSdEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -490,22 +490,16 @@ def infer_unit(value: str) -> tuple[str, str | None]:
|
||||
return value, None
|
||||
|
||||
|
||||
class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
||||
class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
"""Representation of a sensor entity for APCUPSd status values."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: APCUPSdCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, context=description.key.upper())
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
# Initial update of attributes.
|
||||
self._update_attrs()
|
||||
|
||||
@@ -14,7 +14,22 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the APC UPS Daemon",
|
||||
"port": "The port the APC UPS Daemon is listening on"
|
||||
},
|
||||
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
|
||||
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
|
||||
},
|
||||
"description": "[%key:component::apcupsd::config::step::user::description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Virtual integration: Arizona Public Service (APS)."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "aps",
|
||||
"name": "Arizona Public Service (APS)",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import time
|
||||
from typing import Any, Literal, final
|
||||
|
||||
from hassil import Intents, recognize
|
||||
from hassil.expression import Expression, ListReference, Sequence
|
||||
from hassil.expression import Expression, Group, ListReference
|
||||
from hassil.intents import WildcardSlotList
|
||||
|
||||
from homeassistant.components import conversation, media_source, stt, tts
|
||||
@@ -413,7 +413,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
for intent in intents.intents.values():
|
||||
for intent_data in intent.data:
|
||||
for sentence in intent_data.sentences:
|
||||
_collect_list_references(sentence, wildcard_names)
|
||||
_collect_list_references(sentence.expression, wildcard_names)
|
||||
|
||||
for wildcard_name in wildcard_names:
|
||||
intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name)
|
||||
@@ -727,9 +727,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
"""Collect list reference names recursively."""
|
||||
if isinstance(expression, Sequence):
|
||||
seq: Sequence = expression
|
||||
for item in seq.items:
|
||||
if isinstance(expression, Group):
|
||||
grp: Group = expression
|
||||
for item in grp.items:
|
||||
_collect_list_references(item, list_names)
|
||||
elif isinstance(expression, ListReference):
|
||||
# {list}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3"]
|
||||
"requirements": ["hassil==3.2.0"]
|
||||
}
|
||||
|
||||
@@ -5,15 +5,18 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import namedtuple
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
|
||||
from aiohttp import ClientSession
|
||||
from pyasuswrt import AsusWrtError, AsusWrtHttp
|
||||
from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError
|
||||
from asusrouter import AsusRouter, AsusRouterError
|
||||
from asusrouter.config import ARConfigKey
|
||||
from asusrouter.modules.client import AsusClient
|
||||
from asusrouter.modules.data import AsusData
|
||||
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
|
||||
from asusrouter.tools.connection import get_cookie_jar
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -24,7 +27,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
@@ -41,14 +44,13 @@ from .const import (
|
||||
PROTOCOL_HTTPS,
|
||||
PROTOCOL_TELNET,
|
||||
SENSORS_BYTES,
|
||||
SENSORS_CPU,
|
||||
SENSORS_LOAD_AVG,
|
||||
SENSORS_MEMORY,
|
||||
SENSORS_RATES,
|
||||
SENSORS_TEMPERATURES,
|
||||
SENSORS_TEMPERATURES_LEGACY,
|
||||
SENSORS_UPTIME,
|
||||
)
|
||||
from .helpers import clean_dict, translate_to_legacy
|
||||
|
||||
SENSORS_TYPE_BYTES = "sensors_bytes"
|
||||
SENSORS_TYPE_COUNT = "sensors_count"
|
||||
@@ -109,7 +111,10 @@ class AsusWrtBridge(ABC):
|
||||
) -> AsusWrtBridge:
|
||||
"""Get Bridge instance."""
|
||||
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
|
||||
session = async_get_clientsession(hass)
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
cookie_jar=get_cookie_jar(),
|
||||
)
|
||||
return AsusWrtHttpBridge(conf, session)
|
||||
return AsusWrtLegacyBridge(conf, options)
|
||||
|
||||
@@ -310,63 +315,122 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
def __init__(self, conf: dict[str, Any], session: ClientSession) -> None:
|
||||
"""Initialize Bridge that use HTTP library."""
|
||||
super().__init__(conf[CONF_HOST])
|
||||
self._api: AsusWrtHttp = self._get_api(conf, session)
|
||||
# Get API configuration
|
||||
config = self._get_api_config()
|
||||
self._api = self._get_api(conf, session, config)
|
||||
|
||||
@staticmethod
|
||||
def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusWrtHttp:
|
||||
"""Get the AsusWrtHttp API."""
|
||||
return AsusWrtHttp(
|
||||
conf[CONF_HOST],
|
||||
conf[CONF_USERNAME],
|
||||
conf.get(CONF_PASSWORD, ""),
|
||||
use_https=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
|
||||
def _get_api(
|
||||
conf: dict[str, Any], session: ClientSession, config: dict[ARConfigKey, Any]
|
||||
) -> AsusRouter:
|
||||
"""Get the AsusRouter API."""
|
||||
return AsusRouter(
|
||||
hostname=conf[CONF_HOST],
|
||||
username=conf[CONF_USERNAME],
|
||||
password=conf.get(CONF_PASSWORD, ""),
|
||||
use_ssl=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
|
||||
port=conf.get(CONF_PORT),
|
||||
session=session,
|
||||
config=config,
|
||||
)
|
||||
|
||||
def _get_api_config(self) -> dict[ARConfigKey, Any]:
|
||||
"""Get configuration for the API."""
|
||||
return {
|
||||
# Enable automatic temperature data correction in the library
|
||||
ARConfigKey.OPTIMISTIC_TEMPERATURE: True,
|
||||
# Disable `warning`-level log message when temperature
|
||||
# is corrected by setting it to already notified.
|
||||
ARConfigKey.NOTIFIED_OPTIMISTIC_TEMPERATURE: True,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Get connected status."""
|
||||
return cast(bool, self._api.is_connected)
|
||||
return self._api.connected
|
||||
|
||||
async def async_connect(self) -> None:
|
||||
"""Connect to the device."""
|
||||
await self._api.async_connect()
|
||||
|
||||
# Collect the identity
|
||||
_identity = await self._api.async_get_identity()
|
||||
|
||||
# get main router properties
|
||||
if mac := self._api.mac:
|
||||
if mac := _identity.mac:
|
||||
self._label_mac = format_mac(mac)
|
||||
self._firmware = self._api.firmware
|
||||
self._model = self._api.model
|
||||
self._firmware = str(_identity.firmware)
|
||||
self._model = _identity.model
|
||||
|
||||
async def async_disconnect(self) -> None:
|
||||
"""Disconnect to the device."""
|
||||
await self._api.async_disconnect()
|
||||
|
||||
async def _get_data(
|
||||
self,
|
||||
datatype: AsusData,
|
||||
force: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Get data from the device.
|
||||
|
||||
This is a generic method which automatically converts to
|
||||
the Home Assistant-compatible format.
|
||||
"""
|
||||
try:
|
||||
raw = await self._api.async_get_data(datatype, force=force)
|
||||
return translate_to_legacy(clean_dict(convert_to_ha_data(raw)))
|
||||
except AsusRouterError as ex:
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
||||
async def _get_sensors(self, datatype: AsusData) -> list[str]:
|
||||
"""Get the available sensors.
|
||||
|
||||
This is a generic method which automatically converts to
|
||||
the Home Assistant-compatible format.
|
||||
"""
|
||||
sensors = []
|
||||
try:
|
||||
data = await self._api.async_get_data(datatype)
|
||||
# Get the list of sensors from the raw data
|
||||
# and translate in to the legacy format
|
||||
sensors = translate_to_legacy(convert_to_ha_sensors(data, datatype))
|
||||
_LOGGER.debug("Available `%s` sensors: %s", datatype.value, sensors)
|
||||
except AsusRouterError as ex:
|
||||
_LOGGER.warning(
|
||||
"Cannot get available `%s` sensors with exception: %s",
|
||||
datatype.value,
|
||||
ex,
|
||||
)
|
||||
return sensors
|
||||
|
||||
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
|
||||
"""Get list of connected devices."""
|
||||
api_devices = await self._api.async_get_connected_devices()
|
||||
api_devices: dict[str, AsusClient] = await self._api.async_get_data(
|
||||
AsusData.CLIENTS, force=True
|
||||
)
|
||||
return {
|
||||
format_mac(mac): WrtDevice(dev.ip, dev.name, dev.node)
|
||||
format_mac(mac): WrtDevice(
|
||||
dev.connection.ip_address, dev.description.name, dev.connection.node
|
||||
)
|
||||
for mac, dev in api_devices.items()
|
||||
if dev.connection is not None
|
||||
and dev.description is not None
|
||||
and dev.connection.ip_address is not None
|
||||
}
|
||||
|
||||
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return a dictionary of available sensors for this bridge."""
|
||||
sensors_cpu = await self._get_available_cpu_sensors()
|
||||
sensors_temperatures = await self._get_available_temperature_sensors()
|
||||
sensors_loadavg = await self._get_loadavg_sensors_availability()
|
||||
return {
|
||||
SENSORS_TYPE_BYTES: {
|
||||
KEY_SENSORS: SENSORS_BYTES,
|
||||
KEY_METHOD: self._get_bytes,
|
||||
},
|
||||
SENSORS_TYPE_CPU: {
|
||||
KEY_SENSORS: sensors_cpu,
|
||||
KEY_SENSORS: await self._get_sensors(AsusData.CPU),
|
||||
KEY_METHOD: self._get_cpu_usage,
|
||||
},
|
||||
SENSORS_TYPE_LOAD_AVG: {
|
||||
KEY_SENSORS: sensors_loadavg,
|
||||
KEY_SENSORS: await self._get_sensors(AsusData.SYSINFO),
|
||||
KEY_METHOD: self._get_load_avg,
|
||||
},
|
||||
SENSORS_TYPE_MEMORY: {
|
||||
@@ -382,95 +446,44 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
KEY_METHOD: self._get_uptime,
|
||||
},
|
||||
SENSORS_TYPE_TEMPERATURES: {
|
||||
KEY_SENSORS: sensors_temperatures,
|
||||
KEY_SENSORS: await self._get_sensors(AsusData.TEMPERATURE),
|
||||
KEY_METHOD: self._get_temperatures,
|
||||
},
|
||||
}
|
||||
|
||||
async def _get_available_cpu_sensors(self) -> list[str]:
|
||||
"""Check which cpu information is available on the router."""
|
||||
try:
|
||||
available_cpu = await self._api.async_get_cpu_usage()
|
||||
available_sensors = [t for t in SENSORS_CPU if t in available_cpu]
|
||||
except AsusWrtError as exc:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Failed checking cpu sensor availability for ASUS router"
|
||||
" %s. Exception: %s"
|
||||
),
|
||||
self.host,
|
||||
exc,
|
||||
)
|
||||
return []
|
||||
return available_sensors
|
||||
|
||||
async def _get_available_temperature_sensors(self) -> list[str]:
|
||||
"""Check which temperature information is available on the router."""
|
||||
try:
|
||||
available_temps = await self._api.async_get_temperatures()
|
||||
available_sensors = [
|
||||
t for t in SENSORS_TEMPERATURES if t in available_temps
|
||||
]
|
||||
except AsusWrtError as exc:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Failed checking temperature sensor availability for ASUS router"
|
||||
" %s. Exception: %s"
|
||||
),
|
||||
self.host,
|
||||
exc,
|
||||
)
|
||||
return []
|
||||
return available_sensors
|
||||
|
||||
async def _get_loadavg_sensors_availability(self) -> list[str]:
|
||||
"""Check if load avg is available on the router."""
|
||||
try:
|
||||
await self._api.async_get_loadavg()
|
||||
except AsusWrtNotAvailableInfoError:
|
||||
return []
|
||||
except AsusWrtError:
|
||||
pass
|
||||
return SENSORS_LOAD_AVG
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, SENSORS_BYTES)
|
||||
async def _get_bytes(self) -> Any:
|
||||
"""Fetch byte information from the router."""
|
||||
return await self._api.async_get_traffic_bytes()
|
||||
return await self._get_data(AsusData.NETWORK)
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, SENSORS_RATES)
|
||||
async def _get_rates(self) -> Any:
|
||||
"""Fetch rates information from the router."""
|
||||
return await self._api.async_get_traffic_rates()
|
||||
data = await self._get_data(AsusData.NETWORK)
|
||||
# Convert from bits/s to Bytes/s for compatibility with legacy sensors
|
||||
return {
|
||||
key: (
|
||||
value / 8
|
||||
if key in SENSORS_RATES and isinstance(value, (int, float))
|
||||
else value
|
||||
)
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, SENSORS_LOAD_AVG)
|
||||
async def _get_load_avg(self) -> Any:
|
||||
"""Fetch cpu load avg information from the router."""
|
||||
return await self._api.async_get_loadavg()
|
||||
return await self._get_data(AsusData.SYSINFO)
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, None)
|
||||
async def _get_temperatures(self) -> Any:
|
||||
"""Fetch temperatures information from the router."""
|
||||
return await self._api.async_get_temperatures()
|
||||
return await self._get_data(AsusData.TEMPERATURE)
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, None)
|
||||
async def _get_cpu_usage(self) -> Any:
|
||||
"""Fetch cpu information from the router."""
|
||||
return await self._api.async_get_cpu_usage()
|
||||
return await self._get_data(AsusData.CPU)
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, None)
|
||||
async def _get_memory_usage(self) -> Any:
|
||||
"""Fetch memory information from the router."""
|
||||
return await self._api.async_get_memory_usage()
|
||||
return await self._get_data(AsusData.RAM)
|
||||
|
||||
async def _get_uptime(self) -> dict[str, Any]:
|
||||
"""Fetch uptime from the router."""
|
||||
try:
|
||||
uptimes = await self._api.async_get_uptime()
|
||||
except AsusWrtError as exc:
|
||||
raise UpdateFailed(exc) from exc
|
||||
|
||||
last_boot = datetime.fromisoformat(uptimes["last_boot"])
|
||||
uptime = uptimes["uptime"]
|
||||
|
||||
return dict(zip(SENSORS_UPTIME, [last_boot, uptime], strict=False))
|
||||
return await self._get_data(AsusData.BOOTTIME)
|
||||
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
import socket
|
||||
from typing import Any, cast
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
from asusrouter import AsusRouterError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
@@ -189,7 +189,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
await api.async_connect()
|
||||
|
||||
except (AsusWrtError, OSError):
|
||||
except (AsusRouterError, OSError):
|
||||
_LOGGER.error(
|
||||
"Error connecting to the AsusWrt router at %s using protocol %s",
|
||||
host,
|
||||
|
||||
56
homeassistant/components/asuswrt/helpers.py
Normal file
56
homeassistant/components/asuswrt/helpers.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Helpers for AsusWRT integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypeVar
|
||||
|
||||
T = TypeVar("T", dict[str, Any], list[Any], None)
|
||||
|
||||
TRANSLATION_MAP = {
|
||||
"wan_rx": "sensor_rx_bytes",
|
||||
"wan_tx": "sensor_tx_bytes",
|
||||
"total_usage": "cpu_total_usage",
|
||||
"usage": "mem_usage_perc",
|
||||
"free": "mem_free",
|
||||
"used": "mem_used",
|
||||
"wan_rx_speed": "sensor_rx_rates",
|
||||
"wan_tx_speed": "sensor_tx_rates",
|
||||
"2ghz": "2.4GHz",
|
||||
"5ghz": "5.0GHz",
|
||||
"5ghz2": "5.0GHz_2",
|
||||
"6ghz": "6.0GHz",
|
||||
"cpu": "CPU",
|
||||
"datetime": "sensor_last_boot",
|
||||
"uptime": "sensor_uptime",
|
||||
**{f"{num}_usage": f"cpu{num}_usage" for num in range(1, 9)},
|
||||
**{f"load_avg_{load}": f"sensor_load_avg{load}" for load in ("1", "5", "15")},
|
||||
}
|
||||
|
||||
|
||||
def clean_dict(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Cleans dictionary from None values.
|
||||
|
||||
The `state` key is always preserved regardless of its value.
|
||||
"""
|
||||
|
||||
return {k: v for k, v in raw.items() if v is not None or k.endswith("state")}
|
||||
|
||||
|
||||
def translate_to_legacy(raw: T) -> T:
|
||||
"""Translate raw data to legacy format for dicts and lists."""
|
||||
|
||||
if raw is None:
|
||||
return None
|
||||
|
||||
if isinstance(raw, dict):
|
||||
return {TRANSLATION_MAP.get(k, k): v for k, v in raw.items()}
|
||||
|
||||
if isinstance(raw, list):
|
||||
return [
|
||||
TRANSLATION_MAP[item]
|
||||
if isinstance(item, str) and item in TRANSLATION_MAP
|
||||
else item
|
||||
for item in raw
|
||||
]
|
||||
|
||||
return raw
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"domain": "asuswrt",
|
||||
"name": "ASUSWRT",
|
||||
"codeowners": ["@kennedyshead", "@ollo69"],
|
||||
"codeowners": ["@kennedyshead", "@ollo69", "@Vaskivskyi"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioasuswrt", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.21"]
|
||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.1"]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
from asusrouter import AsusRouterError
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
CONF_CONSIDER_HOME,
|
||||
@@ -40,6 +40,9 @@ from .const import (
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AsusWrtConfigEntry
|
||||
|
||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AsusWrtSensorDataHandler:
|
||||
"""Data handler for AsusWrt sensor."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||
) -> None:
|
||||
"""Initialize a AsusWrt sensor data handler."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._entry = entry
|
||||
self._connected_devices = 0
|
||||
|
||||
async def _get_connected_devices(self) -> dict[str, int]:
|
||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
||||
update_method=method,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||
config_entry=self._entry,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -222,7 +229,7 @@ class AsusWrtRouter:
|
||||
"""Set up a AsusWrt router."""
|
||||
try:
|
||||
await self._api.async_connect()
|
||||
except (AsusWrtError, OSError) as exc:
|
||||
except (AsusRouterError, OSError) as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
if not self._api.is_connected:
|
||||
raise ConfigEntryNotReady
|
||||
@@ -277,7 +284,7 @@ class AsusWrtRouter:
|
||||
_LOGGER.debug("Checking devices for ASUS router %s", self.host)
|
||||
try:
|
||||
wrt_devices = await self._api.async_get_connected_devices()
|
||||
except (OSError, AsusWrtError) as exc:
|
||||
except (OSError, AsusRouterError) as exc:
|
||||
if not self._connect_error:
|
||||
self._connect_error = True
|
||||
_LOGGER.error(
|
||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
||||
if self._sensors_data_handler:
|
||||
return
|
||||
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||
self.hass, self._api, self._entry
|
||||
)
|
||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||
|
||||
sensors_types = await self._api.async_get_available_sensors()
|
||||
|
||||
@@ -6,18 +6,21 @@ from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
device_registry as dr,
|
||||
issue_registry as ir,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .const import DEFAULT_AUGUST_BRAND, DOMAIN, PLATFORMS
|
||||
from .data import AugustData
|
||||
from .gateway import AugustGateway
|
||||
from .util import async_create_august_clientsession
|
||||
@@ -25,30 +28,21 @@ from .util import async_create_august_clientsession
|
||||
type AugustConfigEntry = ConfigEntry[AugustData]
|
||||
|
||||
|
||||
@callback
|
||||
def _async_create_yale_brand_migration_issue(
|
||||
hass: HomeAssistant, entry: AugustConfigEntry
|
||||
) -> None:
|
||||
"""Create an issue for a brand migration."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"yale_brand_migration",
|
||||
breaks_in_ha_version="2024.9",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/yale",
|
||||
translation_key="yale_brand_migration",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.CRITICAL,
|
||||
translation_placeholders={
|
||||
"migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||
"""Set up August from a config entry."""
|
||||
# Check if this is a legacy config entry that needs migration to OAuth
|
||||
if "auth_implementation" not in entry.data:
|
||||
# This is a legacy entry using username/password, trigger reauth
|
||||
raise ConfigEntryAuthFailed("Migration to OAuth required")
|
||||
|
||||
session = async_create_august_clientsession(hass)
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
@@ -76,9 +70,7 @@ async def async_setup_august(
|
||||
) -> None:
|
||||
"""Set up the August component."""
|
||||
config = cast(YaleXSConfig, entry.data)
|
||||
await august_gateway.async_setup(config)
|
||||
if august_gateway.api.brand == Brand.YALE_HOME:
|
||||
_async_create_yale_brand_migration_issue(hass, entry)
|
||||
await august_gateway.async_setup({**config, "brand": DEFAULT_AUGUST_BRAND})
|
||||
await august_gateway.async_authenticate()
|
||||
await august_gateway.async_refresh_access_token_if_needed()
|
||||
data = entry.runtime_data = AugustData(hass, august_gateway)
|
||||
|
||||
15
homeassistant/components/august/application_credentials.py
Normal file
15
homeassistant/components/august/application_credentials.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""application_credentials platform for the august integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://auth.august.com/authorization"
|
||||
OAUTH2_TOKEN = "https://auth.august.com/access_token"
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
return AuthorizationServer(
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
||||
@@ -1,284 +1,86 @@
|
||||
"""Config flow for August integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from yalexs.authenticator_common import ValidationResult
|
||||
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
import jwt
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||
CONF_BRAND,
|
||||
CONF_LOGIN_METHOD,
|
||||
DEFAULT_LOGIN_METHOD,
|
||||
DOMAIN,
|
||||
LOGIN_METHODS,
|
||||
VERIFICATION_CODE_KEY,
|
||||
)
|
||||
from .gateway import AugustGateway
|
||||
from .util import async_create_august_clientsession
|
||||
|
||||
# The Yale Home Brand is not supported by the August integration
|
||||
# anymore and should migrate to the Yale integration
|
||||
AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy()
|
||||
del AVAILABLE_BRANDS[Brand.YALE_HOME]
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_validate_input(
|
||||
data: dict[str, Any], august_gateway: AugustGateway
|
||||
) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
|
||||
Request configuration steps from the user.
|
||||
"""
|
||||
assert august_gateway.authenticator is not None
|
||||
authenticator = august_gateway.authenticator
|
||||
if (code := data.get(VERIFICATION_CODE_KEY)) is not None:
|
||||
result = await authenticator.async_validate_verification_code(code)
|
||||
_LOGGER.debug("Verification code validation: %s", result)
|
||||
if result != ValidationResult.VALIDATED:
|
||||
raise RequireValidation
|
||||
|
||||
try:
|
||||
await august_gateway.async_authenticate()
|
||||
except RequireValidation:
|
||||
_LOGGER.debug(
|
||||
"Requesting new verification code for %s via %s",
|
||||
data.get(CONF_USERNAME),
|
||||
data.get(CONF_LOGIN_METHOD),
|
||||
)
|
||||
if code is None:
|
||||
await august_gateway.authenticator.async_send_verification_code()
|
||||
raise
|
||||
|
||||
return {
|
||||
"title": data.get(CONF_USERNAME),
|
||||
"data": august_gateway.config_entry(),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ValidateResult:
|
||||
"""Result from validation."""
|
||||
|
||||
validation_required: bool
|
||||
info: dict[str, Any]
|
||||
errors: dict[str, str]
|
||||
description_placeholders: dict[str, str]
|
||||
|
||||
|
||||
class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class AugustConfigFlow(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
"""Handle a config flow for August."""
|
||||
|
||||
VERSION = 1
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Store an AugustGateway()."""
|
||||
self._august_gateway: AugustGateway | None = None
|
||||
self._aiohttp_session: aiohttp.ClientSession | None = None
|
||||
self._user_auth_details: dict[str, Any] = {}
|
||||
self._needs_reset = True
|
||||
super().__init__()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
return await self.async_step_user_validate()
|
||||
|
||||
async def async_step_user_validate(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle authentication."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._user_auth_details.update(user_input)
|
||||
validate_result = await self._async_auth_or_validate()
|
||||
description_placeholders = validate_result.description_placeholders
|
||||
if validate_result.validation_required:
|
||||
return await self.async_step_validation()
|
||||
if not (errors := validate_result.errors):
|
||||
return await self._async_update_or_create_entry(validate_result.info)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user_validate",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_BRAND,
|
||||
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
||||
): vol.In(AVAILABLE_BRANDS),
|
||||
vol.Required(
|
||||
CONF_LOGIN_METHOD,
|
||||
default=self._user_auth_details.get(
|
||||
CONF_LOGIN_METHOD, DEFAULT_LOGIN_METHOD
|
||||
),
|
||||
): vol.In(LOGIN_METHODS),
|
||||
vol.Required(
|
||||
CONF_USERNAME,
|
||||
default=self._user_auth_details.get(CONF_USERNAME),
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_validation(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle validation (2fa) step."""
|
||||
if user_input:
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self.async_step_reauth_validate(user_input)
|
||||
return await self.async_step_user_validate(user_input)
|
||||
|
||||
previously_failed = VERIFICATION_CODE_KEY in self._user_auth_details
|
||||
return self.async_show_form(
|
||||
step_id="validation",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
|
||||
),
|
||||
errors={"base": "invalid_verification_code"} if previously_failed else None,
|
||||
description_placeholders={
|
||||
CONF_BRAND: self._user_auth_details[CONF_BRAND],
|
||||
CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
|
||||
CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD],
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_gateway(self) -> AugustGateway:
|
||||
"""Set up the gateway."""
|
||||
if self._august_gateway is not None:
|
||||
return self._august_gateway
|
||||
self._aiohttp_session = async_create_august_clientsession(self.hass)
|
||||
self._august_gateway = AugustGateway(
|
||||
Path(self.hass.config.config_dir), self._aiohttp_session
|
||||
)
|
||||
return self._august_gateway
|
||||
|
||||
@callback
|
||||
def _async_shutdown_gateway(self) -> None:
|
||||
"""Shutdown the gateway."""
|
||||
if self._aiohttp_session is not None:
|
||||
self._aiohttp_session.detach()
|
||||
self._august_gateway = None
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return _LOGGER
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._user_auth_details = dict(entry_data)
|
||||
return await self.async_step_reauth_validate()
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reauth_validate(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
def _async_decode_jwt(self, encoded: str) -> dict[str, Any]:
|
||||
"""Decode JWT token."""
|
||||
return jwt.decode(
|
||||
encoded,
|
||||
"",
|
||||
verify=False,
|
||||
options={"verify_signature": False},
|
||||
algorithms=["HS256"],
|
||||
)
|
||||
|
||||
async def _async_handle_reauth(
|
||||
self, data: dict, decoded: dict[str, Any], user_id: str
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth and validation."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._user_auth_details.update(user_input)
|
||||
validate_result = await self._async_auth_or_validate()
|
||||
description_placeholders = validate_result.description_placeholders
|
||||
if validate_result.validation_required:
|
||||
return await self.async_step_validation()
|
||||
if not (errors := validate_result.errors):
|
||||
return await self._async_update_or_create_entry(validate_result.info)
|
||||
"""Handle reauth flow."""
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
assert reauth_entry.unique_id is not None
|
||||
# Check if this is a migration from username (contains @) to user ID
|
||||
if "@" not in reauth_entry.unique_id:
|
||||
# This is a normal oauth reauth, enforce ID matching for security
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_mismatch(reason="reauth_invalid_user")
|
||||
return self.async_update_reload_and_abort(reauth_entry, data=data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_validate",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_BRAND,
|
||||
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
||||
): vol.In(BRANDS_WITHOUT_OAUTH),
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders
|
||||
| {
|
||||
CONF_USERNAME: self._user_auth_details[CONF_USERNAME],
|
||||
},
|
||||
# This is a one-time migration from username to user ID
|
||||
# Only validate if the account has emails
|
||||
emails: list[str]
|
||||
if emails := decoded.get("email", []):
|
||||
# Validate that the email matches before allowing migration
|
||||
email_to_check_lower = reauth_entry.unique_id.casefold()
|
||||
if not any(email.casefold() == email_to_check_lower for email in emails):
|
||||
# Email doesn't match - this is a different account
|
||||
return self.async_abort(reason="reauth_invalid_user")
|
||||
|
||||
# Email matches or no emails on account, update with new unique ID
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry, data=data, unique_id=user_id
|
||||
)
|
||||
|
||||
async def _async_reset_access_token_cache_if_needed(
|
||||
self, gateway: AugustGateway, username: str, access_token_cache_file: str | None
|
||||
) -> None:
|
||||
"""Reset the access token cache if needed."""
|
||||
# We need to configure the access token cache file before we setup the gateway
|
||||
# since we need to reset it if the brand changes BEFORE we setup the gateway
|
||||
gateway.async_configure_access_token_cache_file(
|
||||
username, access_token_cache_file
|
||||
)
|
||||
if self._needs_reset:
|
||||
self._needs_reset = False
|
||||
await gateway.async_reset_authentication()
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
# Decode JWT once
|
||||
access_token = data["token"]["access_token"]
|
||||
decoded = self._async_decode_jwt(access_token)
|
||||
user_id = decoded["userId"]
|
||||
|
||||
async def _async_auth_or_validate(self) -> ValidateResult:
|
||||
"""Authenticate or validate."""
|
||||
user_auth_details = self._user_auth_details
|
||||
gateway = self._async_get_gateway()
|
||||
assert gateway is not None
|
||||
await self._async_reset_access_token_cache_if_needed(
|
||||
gateway,
|
||||
user_auth_details[CONF_USERNAME],
|
||||
user_auth_details.get(CONF_ACCESS_TOKEN_CACHE_FILE),
|
||||
)
|
||||
await gateway.async_setup(user_auth_details)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self._async_handle_reauth(data, decoded, user_id)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
info: dict[str, Any] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
validation_required = False
|
||||
|
||||
try:
|
||||
info = await async_validate_input(user_auth_details, gateway)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except RequireValidation:
|
||||
validation_required = True
|
||||
except Exception as ex:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unhandled"
|
||||
description_placeholders = {"error": str(ex)}
|
||||
|
||||
return ValidateResult(
|
||||
validation_required, info, errors, description_placeholders
|
||||
)
|
||||
|
||||
async def _async_update_or_create_entry(
|
||||
self, info: dict[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Update existing entry or create a new one."""
|
||||
self._async_shutdown_gateway()
|
||||
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
self._user_auth_details[CONF_USERNAME]
|
||||
)
|
||||
if not existing_entry:
|
||||
return self.async_create_entry(title=info["title"], data=info["data"])
|
||||
|
||||
return self.async_update_reload_and_abort(existing_entry, data=info["data"])
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await super().async_oauth_create_entry(data)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Constants for August devices."""
|
||||
|
||||
from yalexs.const import Brand
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DEFAULT_TIMEOUT = 25
|
||||
@@ -9,6 +11,8 @@ CONF_BRAND = "brand"
|
||||
CONF_LOGIN_METHOD = "login_method"
|
||||
CONF_INSTALL_ID = "install_id"
|
||||
|
||||
DEFAULT_AUGUST_BRAND = Brand.YALE_AUGUST
|
||||
|
||||
VERIFICATION_CODE_KEY = "verification_code"
|
||||
|
||||
NOTIFICATION_ID = "august_notification"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user