mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 10:29:27 +00:00
Compare commits
893 Commits
openai-mod
...
synesthesi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a3abba00 | ||
|
|
f6b39d5c4b | ||
|
|
082fe20ffb | ||
|
|
5f7c634162 | ||
|
|
9c9aa23e62 | ||
|
|
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 | ||
|
|
72e78b6719 | ||
|
|
a50b035479 | ||
|
|
e175e3ed0b | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
91f6b8e1fe | ||
|
|
1a9d1a9649 | ||
|
|
cb7c7767b5 | ||
|
|
d02029143c | ||
|
|
3eda687d30 | ||
|
|
7688c367cc | ||
|
|
a1dc3f3eac | ||
|
|
d135d08813 | ||
|
|
9595759fd1 | ||
|
|
d54f979612 | ||
|
|
531073acc0 | ||
|
|
73cbc962f9 | ||
|
|
34b0b71375 | ||
|
|
203c908730 | ||
|
|
23e6148d3b | ||
|
|
2a5a66f9d5 | ||
|
|
84ce5d65e1 | ||
|
|
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 | ||
|
|
757fee9f73 | ||
|
|
06130219b4 | ||
|
|
4e2fe63182 | ||
|
|
d0cc9990dd | ||
|
|
76ca9ce3a4 | ||
|
|
124e7cf4c8 | ||
|
|
260ea9a3be | ||
|
|
e1f6820cb6 | ||
|
|
2215777cfb | ||
|
|
fa3ce62ae8 | ||
|
|
33421bddf3 | ||
|
|
1efe2b437d | ||
|
|
a54f0adf74 | ||
|
|
afe574f74e | ||
|
|
25aae8944d | ||
|
|
f26e6ad211 | ||
|
|
e9444a2e4d | ||
|
|
60988534a9 | ||
|
|
932bf81ac8 | ||
|
|
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 | ||
|
|
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 | ||
|
|
1a54d566f8 | ||
|
|
1a9cae0f89 | ||
|
|
551dcaa169 | ||
|
|
5467db065b | ||
|
|
6a8d752e56 | ||
|
|
179a56628d | ||
|
|
b3f830773a | ||
|
|
084e06ec7d | ||
|
|
e0190afd3c | ||
|
|
b9e16d54c4 | ||
|
|
627785edc1 | ||
|
|
4318e29ce8 | ||
|
|
fea5c63bba | ||
|
|
b2349ac2bd | ||
|
|
08f7b708a4 | ||
|
|
1236801b7d | ||
|
|
72d9dbf39d | ||
|
|
755864f9f3 | ||
|
|
fa476d4e34 | ||
|
|
018197e41a | ||
|
|
7dd2b9e422 | ||
|
|
3e615fd373 | ||
|
|
c0bf167e10 | ||
|
|
45f6778ff4 | ||
|
|
bddd4d621a | ||
|
|
b0e75e9ee4 | ||
|
|
d45c03a795 | ||
|
|
8562c8d32f | ||
|
|
ae42d71123 | ||
|
|
9616c8cd7b | ||
|
|
9394546668 | ||
|
|
d43f21c2e2 | ||
|
|
8d68fee9f8 | ||
|
|
b4a4e218ec | ||
|
|
fb2d62d692 | ||
|
|
f538807d6e | ||
|
|
a08c3c9f44 | ||
|
|
506431c75f | ||
|
|
37579440e6 | ||
|
|
5ce2729dc2 | ||
|
|
b5e4ae4a53 | ||
|
|
3d4386ea6d | ||
|
|
9f1cec893e | ||
|
|
bc87140a6f | ||
|
|
d77a3fca83 | ||
|
|
924a86dfb6 | ||
|
|
0d7608f7c5 | ||
|
|
22e054f4cd | ||
|
|
8b53b26333 | ||
|
|
4d59e8cd80 | ||
|
|
61396d92a5 | ||
|
|
c72c600de4 | ||
|
|
b86b0c10bd | ||
|
|
eb222f6c5d | ||
|
|
4b5fe424ed | ||
|
|
61ca42e923 | ||
|
|
21c1427abf | ||
|
|
aa6b37bc7c | ||
|
|
bbc1466cfc | ||
|
|
21a9799060 | ||
|
|
f7d54b46ec | ||
|
|
6ad1b8dcb1 | ||
|
|
5f6b1212a3 | ||
|
|
58dc6a952e | ||
|
|
59d8df142d | ||
|
|
04fb86b4ba | ||
|
|
3d744f032f | ||
|
|
f7c8cdb3a7 | ||
|
|
3952544822 | ||
|
|
42101dd432 | ||
|
|
f7eacaa48d | ||
|
|
ad0db5c83a | ||
|
|
63216b77c2 | ||
|
|
7a55373b0b | ||
|
|
f9e7459901 | ||
|
|
94dc2e2ea3 | ||
|
|
2cf144fb25 | ||
|
|
f318766021 | ||
|
|
ec7fb140ac | ||
|
|
2706c7d67d | ||
|
|
b4e50902eb | ||
|
|
1ead01bc9a | ||
|
|
389a1251a1 | ||
|
|
8d27ca1e21 | ||
|
|
a76af50c10 | ||
|
|
09b91bd76a | ||
|
|
736d582d04 | ||
|
|
8114df4219 | ||
|
|
8193259e02 | ||
|
|
6306baa3c9 | ||
|
|
d481a694f1 | ||
|
|
edca3fc0b7 | ||
|
|
daea76c2f1 | ||
|
|
160b61e0b9 | ||
|
|
fc900a632a | ||
|
|
1b58809655 | ||
|
|
223c34056d | ||
|
|
99ee56a4dd | ||
|
|
91be25a292 | ||
|
|
a21af78aa1 | ||
|
|
70cfdfa231 | ||
|
|
a5b075af68 | ||
|
|
c4d4ef884e | ||
|
|
ba4e7e50e0 | ||
|
|
dd0b23afb0 | ||
|
|
779f0afcc4 | ||
|
|
d8016f7f41 | ||
|
|
25169e9075 | ||
|
|
260ca70785 | ||
|
|
69e3a5bc34 | ||
|
|
1a75a88c76 | ||
|
|
6c2a662838 | ||
|
|
749fc318ca | ||
|
|
828f979c78 | ||
|
|
1eb6d5fe32 | ||
|
|
5930ac6425 | ||
|
|
15e45df8a7 | ||
|
|
a79d2da9a3 | ||
|
|
ac86f2e2ba | ||
|
|
03ee97d38f | ||
|
|
06233b5134 | ||
|
|
9d66b19c03 | ||
|
|
bb6bcfdd01 | ||
|
|
8e9e304608 | ||
|
|
6b641411a0 | ||
|
|
6f8214bbb4 | ||
|
|
f66e83f33e | ||
|
|
2ee82e1d6f | ||
|
|
0dd1e0cabb | ||
|
|
45ae34cc0e | ||
|
|
73e578b168 | ||
|
|
52ee5d53ee | ||
|
|
62713b1371 | ||
|
|
c4c4463c63 | ||
|
|
7e2fd6e47b | ||
|
|
9f45801409 | ||
|
|
aaec243bf4 | ||
|
|
b67e85e8da | ||
|
|
25407c0f4b | ||
|
|
09e7d8d1a5 | ||
|
|
ff7c125334 | ||
|
|
3d6f868cbc | ||
|
|
378c3af9df | ||
|
|
c7271d1af9 | ||
|
|
87400c6a17 | ||
|
|
692a1119a6 | ||
|
|
2e728eb7de | ||
|
|
45ec9c7dad | ||
|
|
62ee1fbc64 | ||
|
|
3c1aa9d9de | ||
|
|
bf568b22d7 | ||
|
|
596f6cd216 | ||
|
|
cf05f1046d | ||
|
|
7f9be420d2 | ||
|
|
dda46e7e0b | ||
|
|
b1dd742a57 | ||
|
|
5af4290b77 | ||
|
|
8339516fb4 | ||
|
|
aa1314c1d5 | ||
|
|
92ad922ddc | ||
|
|
e518e7beac | ||
|
|
483d814a8f | ||
|
|
8f795f021c | ||
|
|
d823b574c0 | ||
|
|
49bd15718c | ||
|
|
d3f18c1678 | ||
|
|
5ef17c8588 | ||
|
|
e8b8d31027 | ||
|
|
978ee3870c | ||
|
|
b3862591ea | ||
|
|
1895db0ddd | ||
|
|
ee2cf961f6 | ||
|
|
9a364ec729 | ||
|
|
96529ec245 | ||
|
|
8fc8220924 | ||
|
|
386f709fd3 | ||
|
|
d088fccb88 | ||
|
|
2a5448835f | ||
|
|
a71eecaaa4 | ||
|
|
46d810b9f9 | ||
|
|
48c4240a5d | ||
|
|
bf05c23414 | ||
|
|
db1e6a0d98 | ||
|
|
4ad35e8421 | ||
|
|
850e04d9aa | ||
|
|
95c5a91f01 | ||
|
|
140f56aeaa | ||
|
|
40ce228c9c | ||
|
|
18c5437fe7 | ||
|
|
ebad1ff4cc | ||
|
|
a68e722c92 | ||
|
|
05935bbc01 | ||
|
|
c67636b4f6 | ||
|
|
777b3128bb | ||
|
|
ab6cd0eb41 | ||
|
|
f35558413a | ||
|
|
e30d405625 | ||
|
|
622cce03a1 | ||
|
|
1fa9141ce1 | ||
|
|
a060f7486f | ||
|
|
dbb5730389 | ||
|
|
431b2aa1d5 | ||
|
|
c99d81a554 | ||
|
|
ff4dc393cf | ||
|
|
d384bee576 | ||
|
|
f0cb5d5480 | ||
|
|
725799c73e | ||
|
|
dc6d2e3e84 | ||
|
|
4a7d06a68a | ||
|
|
cd800da357 | ||
|
|
4c8ab8eb64 | ||
|
|
60f4d29d60 | ||
|
|
68b7d09476 | ||
|
|
c3eb6dea11 | ||
|
|
f428ffde87 | ||
|
|
fa207860a0 | ||
|
|
959c3a8a99 | ||
|
|
254ccca4e5 | ||
|
|
5b08724d81 | ||
|
|
ea2b3b3ff3 | ||
|
|
a33760bc1a | ||
|
|
4ea7ad52b1 | ||
|
|
dac75d1902 | ||
|
|
0e9ced3c00 | ||
|
|
22d0fbcbd2 | ||
|
|
57b641b97d | ||
|
|
27bd6d2e38 | ||
|
|
427e5d81df | ||
|
|
b6bd92ed19 | ||
|
|
7976729e76 | ||
|
|
5aa0d0dc81 | ||
|
|
e1501d7510 | ||
|
|
be5109fddf | ||
|
|
c5cf9b07b7 | ||
|
|
002b7c6789 | ||
|
|
e017dc80a0 | ||
|
|
aab7381553 | ||
|
|
cbf4409db3 | ||
|
|
56fb59e48e | ||
|
|
971bd56bee | ||
|
|
b2710c1bce | ||
|
|
a069b59efc | ||
|
|
02eb1dd533 | ||
|
|
b3130c7929 | ||
|
|
aad1dbecb4 | ||
|
|
65109ea000 | ||
|
|
356ac74fa5 | ||
|
|
f3513f7f29 | ||
|
|
4bbb94f43d | ||
|
|
c1fa721a57 | ||
|
|
e3ffb41650 | ||
|
|
123cce6d96 | ||
|
|
6920dec352 | ||
|
|
f7cc260336 | ||
|
|
b7da31a021 | ||
|
|
95d4dc678c | ||
|
|
7e9da052ca | ||
|
|
59ece455d9 | ||
|
|
3ba144c8b2 | ||
|
|
456f992b7e | ||
|
|
0675e34c62 | ||
|
|
190c98f5a8 | ||
|
|
c6bb26be89 | ||
|
|
d57c5ffa8f | ||
|
|
68889e1790 | ||
|
|
8fdc50a29f | ||
|
|
5656b4c20d | ||
|
|
b6edcc9422 | ||
|
|
7a3eb53453 | ||
|
|
11a2c73e8a | ||
|
|
1644484c92 | ||
|
|
8e0a89dc2f | ||
|
|
9e4b8df344 | ||
|
|
69fdc1d269 | ||
|
|
56e0aa103d | ||
|
|
caf0492009 | ||
|
|
c6d0aad3d3 | ||
|
|
1f59b735c6 | ||
|
|
87af9fc8ba | ||
|
|
691a0ca065 | ||
|
|
80384b89a5 | ||
|
|
f7672985ed | ||
|
|
d4374dbcc7 | ||
|
|
c4ddcd64c8 | ||
|
|
c802430066 | ||
|
|
649fbfc729 | ||
|
|
80c52ad8ea | ||
|
|
150d4716fa | ||
|
|
dc2736580f | ||
|
|
f1272ef513 | ||
|
|
3c2fa023b4 | ||
|
|
5cf5be8c9c | ||
|
|
63b21fda1a | ||
|
|
d87379d083 | ||
|
|
0990cef917 | ||
|
|
962ad99c20 | ||
|
|
9c9836defd | ||
|
|
e951fc401c | ||
|
|
00e2a177a5 | ||
|
|
b6d316c8f2 | ||
|
|
b8425de0d0 | ||
|
|
d51a44acbc | ||
|
|
435465e569 | ||
|
|
3b047859f9 | ||
|
|
91cdf1a367 | ||
|
|
2377b136f3 | ||
|
|
186c4e7038 | ||
|
|
d303a7d17e | ||
|
|
14f059c766 | ||
|
|
4a10370932 | ||
|
|
672ffa5984 | ||
|
|
3d3f2527cb | ||
|
|
5c3b279f95 | ||
|
|
59bcf1167a | ||
|
|
b4d789f8e2 | ||
|
|
f4ca56052b | ||
|
|
74f9549431 | ||
|
|
9650727515 | ||
|
|
c965da6559 | ||
|
|
9077965214 | ||
|
|
2b7992e849 | ||
|
|
5d6b02f470 | ||
|
|
a274961593 | ||
|
|
4e163c4591 | ||
|
|
3ffec2a655 | ||
|
|
c646658643 | ||
|
|
342b4c3442 | ||
|
|
eb58c10e5e | ||
|
|
f42e7d982f | ||
|
|
898ef43750 | ||
|
|
f806e6ba49 | ||
|
|
c23bfb1b39 | ||
|
|
a2ffe32b02 | ||
|
|
0f32b6331d | ||
|
|
9a4959560e | ||
|
|
41ab7b346c |
28
.github/workflows/builder.yml
vendored
28
.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 }}
|
||||
|
||||
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.8"
|
||||
HA_SHORT_VERSION: "2025.9"
|
||||
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.2
|
||||
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.4
|
||||
uses: github/codeql-action/init@v3.29.11
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.4
|
||||
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.0
|
||||
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.0
|
||||
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
|
||||
|
||||
24
.github/workflows/wheels.yml
vendored
24
.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
|
||||
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -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
|
||||
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -53,6 +53,7 @@ homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airos.*
|
||||
homeassistant.components.airq.*
|
||||
homeassistant.components.airthings.*
|
||||
homeassistant.components.airthings_ble.*
|
||||
@@ -309,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.*
|
||||
@@ -466,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.*
|
||||
@@ -501,6 +502,7 @@ homeassistant.components.tag.*
|
||||
homeassistant.components.tailscale.*
|
||||
homeassistant.components.tailwind.*
|
||||
homeassistant.components.tami4.*
|
||||
homeassistant.components.tankerkoenig.*
|
||||
homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
@@ -546,6 +548,7 @@ homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
homeassistant.components.volvo.*
|
||||
homeassistant.components.wake_on_lan.*
|
||||
homeassistant.components.wake_word.*
|
||||
homeassistant.components.wallbox.*
|
||||
|
||||
22
CODEOWNERS
generated
22
CODEOWNERS
generated
@@ -67,6 +67,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airly/ @bieniu
|
||||
/homeassistant/components/airnow/ @asymworks
|
||||
/tests/components/airnow/ @asymworks
|
||||
/homeassistant/components/airos/ @CoMPaTech
|
||||
/tests/components/airos/ @CoMPaTech
|
||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||
/tests/components/airq/ @Sibgatulin @dl2080
|
||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||
@@ -154,8 +156,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
|
||||
@@ -420,6 +422,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
|
||||
@@ -436,8 +440,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
|
||||
@@ -860,8 +864,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
|
||||
@@ -1415,6 +1417,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
|
||||
@@ -1597,6 +1601,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
|
||||
@@ -1611,8 +1617,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
|
||||
@@ -1706,6 +1710,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
/tests/components/volumio/ @OnFreund
|
||||
/homeassistant/components/volvo/ @thomasddn
|
||||
/tests/components/volvo/ @thomasddn
|
||||
/homeassistant/components/volvooncall/ @molobrakos
|
||||
/tests/components/volvooncall/ @molobrakos
|
||||
/homeassistant/components/vulcan/ @Antoni-Czaplicki
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False):
|
||||
redirect_uri: str
|
||||
|
||||
|
||||
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
|
||||
class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False):
|
||||
"""Typed result dict for auth flow."""
|
||||
|
||||
result: Credentials # Only present if type is CREATE_ENTRY
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
|
||||
5
homeassistant/brands/frient.json
Normal file
5
homeassistant/brands/frient.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "frient",
|
||||
"name": "Frient",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "third_reality",
|
||||
"name": "Third Reality",
|
||||
"iot_standards": ["zigbee"]
|
||||
"iot_standards": ["matter", "zigbee"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "ubiquiti",
|
||||
"name": "Ubiquiti",
|
||||
"integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"display_pm_standard": {
|
||||
"name": "Display PM standard",
|
||||
"state": {
|
||||
"ugm3": "µg/m³",
|
||||
"ugm3": "μg/m³",
|
||||
"us_aqi": "US AQI"
|
||||
}
|
||||
},
|
||||
|
||||
45
homeassistant/components/airos/__init__.py
Normal file
45
homeassistant/components/airos/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""The Ubiquiti airOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from airos.airos8 import AirOS
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Set up Ubiquiti airOS from a config entry."""
|
||||
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# 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(
|
||||
host=entry.data[CONF_HOST],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
|
||||
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
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 AirOSConfigEntry, AirOSData, 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[[AirOSData], 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)
|
||||
82
homeassistant/components/airos/config_flow.py
Normal file
82
homeassistant/components/airos/config_flow.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Config flow for the Ubiquiti airOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ubiquiti airOS."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# 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(
|
||||
host=user_input[CONF_HOST],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
try:
|
||||
await airos_device.login()
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(airos_data.derived.mac)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=airos_data.host.hostname, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
9
homeassistant/components/airos/const.py
Normal file
9
homeassistant/components/airos/const.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Constants for the Ubiquiti airOS integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "airos"
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
MANUFACTURER = "Ubiquiti"
|
||||
70
homeassistant/components/airos/coordinator.py
Normal file
70
homeassistant/components/airos/coordinator.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""DataUpdateCoordinator for AirOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
"""Class to manage fetching AirOS data from single endpoint."""
|
||||
|
||||
config_entry: AirOSConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.airos_device = airos_device
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> AirOSData:
|
||||
"""Fetch data from AirOS."""
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from 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 (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_data_missing",
|
||||
) from err
|
||||
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
||||
36
homeassistant/components/airos/entity.py
Normal file
36
homeassistant/components/airos/entity.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Generic AirOS Entity Class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import AirOSDataUpdateCoordinator
|
||||
|
||||
|
||||
class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
||||
"""Represent a AirOS Entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None:
|
||||
"""Initialise the gateway."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
airos_data = self.coordinator.data
|
||||
|
||||
configuration_url: str | None = (
|
||||
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
||||
)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
|
||||
configuration_url=configuration_url,
|
||||
identifiers={(DOMAIN, str(airos_data.host.device_id))},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=airos_data.host.devmodel,
|
||||
name=airos_data.host.hostname,
|
||||
sw_version=airos_data.host.fwversion,
|
||||
)
|
||||
10
homeassistant/components/airos/manifest.json
Normal file
10
homeassistant/components/airos/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "airos",
|
||||
"name": "Ubiquiti airOS",
|
||||
"codeowners": ["@CoMPaTech"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.4.3"]
|
||||
}
|
||||
70
homeassistant/components/airos/quality_scale.yaml
Normal file
70
homeassistant/components/airos/quality_scale.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: airOS does not have actions
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: airOS does not have actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: local_polling without 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: airOS does not have actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: no (custom) icons used or envisioned
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
194
homeassistant/components/airos/sensor.py
Normal file
194
homeassistant/components/airos/sensor.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""AirOS Sensor component for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from airos.data import DerivedWirelessMode, DerivedWirelessRole, NetRole
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
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 .entity import AirOSEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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]
|
||||
|
||||
|
||||
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
AirOSSensorEntityDescription(
|
||||
key="host_cpuload",
|
||||
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,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="host_netrole",
|
||||
translation_key="host_netrole",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.host.netrole.value,
|
||||
options=NETROLE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_frequency",
|
||||
translation_key="wireless_frequency",
|
||||
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.frequency,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_essid",
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.antenna_gain,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_throughput_tx",
|
||||
translation_key="wireless_throughput_tx",
|
||||
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(
|
||||
key="wireless_throughput_rx",
|
||||
translation_key="wireless_throughput_rx",
|
||||
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(
|
||||
key="wireless_polling_dl_capacity",
|
||||
translation_key="wireless_polling_dl_capacity",
|
||||
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(
|
||||
key="wireless_polling_ul_capacity",
|
||||
translation_key="wireless_polling_ul_capacity",
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirOSConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the AirOS sensors from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS)
|
||||
|
||||
|
||||
class AirOSSensor(AirOSEntity, SensorEntity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
entity_description: AirOSSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirOSDataUpdateCoordinator,
|
||||
description: AirOSSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
117
homeassistant/components/airos/strings.json
Normal file
117
homeassistant/components/airos/strings.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Ubiquiti airOS device",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "IP address or hostname of the airOS device",
|
||||
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||
"password": "Password configured through the UISP app or web interface"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"host_netrole": {
|
||||
"name": "Network role",
|
||||
"state": {
|
||||
"bridge": "Bridge",
|
||||
"router": "Router"
|
||||
}
|
||||
},
|
||||
"wireless_frequency": {
|
||||
"name": "Wireless frequency"
|
||||
},
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
"wireless_throughput_tx": {
|
||||
"name": "Throughput transmit (actual)"
|
||||
},
|
||||
"wireless_throughput_rx": {
|
||||
"name": "Throughput receive (actual)"
|
||||
},
|
||||
"wireless_polling_dl_capacity": {
|
||||
"name": "Download capacity"
|
||||
},
|
||||
"wireless_polling_ul_capacity": {
|
||||
"name": "Upload capacity"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_auth": {
|
||||
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"key_data_missing": {
|
||||
"message": "Key data not returned from device"
|
||||
},
|
||||
"error_data_missing": {
|
||||
"message": "Data incomplete or missing"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.0"]
|
||||
"requirements": ["aioairzone-cloud==0.7.1"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"""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 _LOGGER, COUNTRY_DOMAINS, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -12,11 +16,20 @@ PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Alexa Devices component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Alexa Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry, session)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -27,10 +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 == 0:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
# Convert country in domain
|
||||
country = entry.data[CONF_COUNTRY]
|
||||
domain = COUNTRY_DOMAINS.get(country, country)
|
||||
|
||||
# Save domain and remove country
|
||||
new_data = entry.data.copy()
|
||||
new_data.update({"site": f"https://www.amazon.{domain}"})
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, version=1, minor_version=1
|
||||
)
|
||||
|
||||
_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."""
|
||||
coordinator = entry.runtime_data
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await coordinator.api.close()
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -10,15 +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
|
||||
|
||||
@@ -28,28 +27,33 @@ 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]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
api = AmazonEchoApi(
|
||||
data[CONF_COUNTRY],
|
||||
session,
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
||||
finally:
|
||||
await api.close()
|
||||
|
||||
return data
|
||||
return await api.login_mode_interactive(data[CONF_CODE])
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -60,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()
|
||||
@@ -80,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,
|
||||
@@ -111,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"
|
||||
@@ -131,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 = {"domain": "com"}
|
||||
COUNTRY_DOMAINS = {
|
||||
"ar": DEFAULT_DOMAIN,
|
||||
"at": DEFAULT_DOMAIN,
|
||||
"au": {"domain": "com.au"},
|
||||
"be": {"domain": "com.be"},
|
||||
"br": DEFAULT_DOMAIN,
|
||||
"gb": {"domain": "co.uk"},
|
||||
"il": DEFAULT_DOMAIN,
|
||||
"jp": {"domain": "co.jp"},
|
||||
"mx": {"domain": "com.mx"},
|
||||
"no": DEFAULT_DOMAIN,
|
||||
"nz": {"domain": "com.au"},
|
||||
"pl": DEFAULT_DOMAIN,
|
||||
"tr": {"domain": "com.tr"},
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": {"domain": "co.za"},
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ from aioamazondevices.exceptions import (
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
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
|
||||
@@ -31,6 +32,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
super().__init__(
|
||||
@@ -41,7 +43,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
entry.data[CONF_COUNTRY],
|
||||
session,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
@@ -64,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",
|
||||
|
||||
@@ -38,5 +38,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sound": {
|
||||
"service": "mdi:cast-audio"
|
||||
},
|
||||
"send_text_command": {
|
||||
"service": "mdi:microphone-message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==3.5.1"]
|
||||
"requirements": ["aioamazondevices==5.0.0"]
|
||||
}
|
||||
|
||||
@@ -48,19 +48,19 @@ rules:
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
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
|
||||
@@ -70,5 +70,5 @@ rules:
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
||||
121
homeassistant/components/alexa_devices/services.py
Normal file
121
homeassistant/components/alexa_devices/services.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Support for services."""
|
||||
|
||||
from aioamazondevices.sounds import SOUNDS_LIST
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
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,
|
||||
},
|
||||
)
|
||||
SCHEMA_CUSTOM_COMMAND = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_TEXT_COMMAND): cv.string,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_entry_id_for_service_call(
|
||||
call: ServiceCall,
|
||||
) -> tuple[dr.DeviceEntry, AmazonConfigEntry]:
|
||||
"""Get the entry ID related to a service call (by device ID)."""
|
||||
device_registry = dr.async_get(call.hass)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
if (device_entry := device_registry.async_get(device_id)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
for entry_id in device_entry.config_entries:
|
||||
if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None:
|
||||
continue
|
||||
if entry.domain == DOMAIN:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
translation_placeholders={"entry": entry.title},
|
||||
)
|
||||
return (device_entry, entry)
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_found",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
|
||||
async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
"""Execute action on the device."""
|
||||
device, config_entry = async_get_entry_id_for_service_call(call)
|
||||
assert device.serial_number
|
||||
value: str = call.data[attribute]
|
||||
|
||||
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]:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_sound_value",
|
||||
translation_placeholders={"sound": value, "variant": str(variant)},
|
||||
)
|
||||
await coordinator.api.call_alexa_sound(
|
||||
coordinator.data[device.serial_number], file
|
||||
)
|
||||
elif attribute == ATTR_TEXT_COMMAND:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
|
||||
|
||||
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||
"""Send a sound notification to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_SOUND)
|
||||
|
||||
|
||||
async def async_send_text_command(call: ServiceCall) -> None:
|
||||
"""Send a custom command to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_TEXT_COMMAND)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Amazon Devices integration."""
|
||||
for service_name, method, schema in (
|
||||
(
|
||||
SERVICE_SOUND_NOTIFICATION,
|
||||
async_send_sound_notification,
|
||||
SCHEMA_SOUND_SERVICE,
|
||||
),
|
||||
(
|
||||
SERVICE_TEXT_COMMAND,
|
||||
async_send_text_command,
|
||||
SCHEMA_CUSTOM_COMMAND,
|
||||
),
|
||||
):
|
||||
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
||||
504
homeassistant/components/alexa_devices/services.yaml
Normal file
504
homeassistant/components/alexa_devices/services.yaml
Normal file
@@ -0,0 +1,504 @@
|
||||
send_text_command:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
text_command:
|
||||
required: true
|
||||
example: "Play B.B.C. on TuneIn"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_sound:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
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
|
||||
default: amzn_sfx_doorbell_chime
|
||||
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
|
||||
translation_key: sound
|
||||
@@ -1,23 +1,21 @@
|
||||
{
|
||||
"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."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
|
||||
"device_id_description": "The ID of the device to send the command to."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
"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%]"
|
||||
@@ -32,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": {
|
||||
@@ -39,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%]"
|
||||
}
|
||||
},
|
||||
@@ -84,12 +92,532 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sound": {
|
||||
"name": "Send sound",
|
||||
"description": "Sends a sound to a device",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device",
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||
},
|
||||
"sound": {
|
||||
"name": "Alexa Skill sound file",
|
||||
"description": "The sound file to play."
|
||||
},
|
||||
"sound_variant": {
|
||||
"name": "Sound variant",
|
||||
"description": "The variant of the sound to play."
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_text_command": {
|
||||
"name": "Send text command",
|
||||
"description": "Sends a text command to a device",
|
||||
"fields": {
|
||||
"text_command": {
|
||||
"name": "Alexa text command",
|
||||
"description": "The text command to send."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device",
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect_with_error": {
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_retrieve_data_with_error": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"device_serial_number_missing": {
|
||||
"message": "Device serial number missing: {device_id}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
"invalid_sound_value": {
|
||||
"message": "Invalid sound {sound} with variant {variant} 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(
|
||||
{
|
||||
@@ -430,11 +418,12 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"hw_version": device.hw_version,
|
||||
"has_suggested_area": device.suggested_area is not None,
|
||||
"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
|
||||
|
||||
@@ -454,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 don't update the disabled_by flag
|
||||
# when moving a device or entity from one config entry to another,
|
||||
# so we need to do it manually.
|
||||
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 don't update the disabled_by flag when
|
||||
# moving a device or entity from one config entry to another, so we
|
||||
# need to do it manually.
|
||||
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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
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%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,17 @@ 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.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 +26,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 +43,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 +110,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,16 +314,16 @@ 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)
|
||||
self._api = self._get_api(conf, session)
|
||||
|
||||
@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) -> 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,
|
||||
)
|
||||
@@ -327,46 +331,90 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
@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 +430,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.0"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
"""Handle August connection setup and authentication."""
|
||||
|
||||
from typing import Any
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from yalexs.const import DEFAULT_BRAND
|
||||
from aiohttp import ClientSession
|
||||
from yalexs.authenticator_common import Authentication, AuthenticationState
|
||||
from yalexs.manager.gateway import Gateway
|
||||
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||
CONF_BRAND,
|
||||
CONF_INSTALL_ID,
|
||||
CONF_LOGIN_METHOD,
|
||||
)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AugustGateway(Gateway):
|
||||
"""Handle the connection to August."""
|
||||
|
||||
def config_entry(self) -> dict[str, Any]:
|
||||
"""Config entry."""
|
||||
assert self._config is not None
|
||||
return {
|
||||
CONF_BRAND: self._config.get(CONF_BRAND, DEFAULT_BRAND),
|
||||
CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD],
|
||||
CONF_USERNAME: self._config[CONF_USERNAME],
|
||||
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
|
||||
CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
config_path: Path,
|
||||
aiohttp_session: ClientSession,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
) -> None:
|
||||
"""Init the connection."""
|
||||
super().__init__(config_path, aiohttp_session)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Get access token."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
return self._oauth_session.token["access_token"]
|
||||
|
||||
async def async_refresh_access_token_if_needed(self) -> None:
|
||||
"""Refresh the access token if needed."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
async def async_authenticate(self) -> Authentication:
|
||||
"""Authenticate with the details provided to setup."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
self.authentication = Authentication(
|
||||
AuthenticationState.AUTHENTICATED, None, None, None
|
||||
)
|
||||
return self.authentication
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "August",
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials", "cloud"],
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "connect",
|
||||
@@ -28,5 +29,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
|
||||
"requirements": ["yalexs==8.12.0", "yalexs-ble==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -6,42 +6,34 @@
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"error": {
|
||||
"unhandled": "Unhandled error: {error}",
|
||||
"invalid_verification_code": "Invalid verification code",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
|
||||
"data": {
|
||||
"implementation": "[%key:common::config_flow::data::implementation%]"
|
||||
},
|
||||
"data_description": {
|
||||
"implementation": "[%key:common::config_flow::description::implementation%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
|
||||
"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%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"reauth_invalid_user": "Reauthenticate must use the same account."
|
||||
},
|
||||
"step": {
|
||||
"validation": {
|
||||
"title": "Two-factor authentication",
|
||||
"data": {
|
||||
"verification_code": "Verification code"
|
||||
},
|
||||
"description": "Please check your {login_method} ({username}) and enter the verification code below. Codes may take a few minutes to arrive."
|
||||
},
|
||||
"user_validate": {
|
||||
"description": "It is recommended to use the 'email' login method as some brands may not work with the 'phone' method. If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.",
|
||||
"data": {
|
||||
"brand": "Brand",
|
||||
"login_method": "Login Method",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"title": "Set up an August account"
|
||||
},
|
||||
"reauth_validate": {
|
||||
"description": "Choose the correct brand for your device, and enter the password for {username}. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.",
|
||||
"data": {
|
||||
"brand": "[%key:component::august::config::step::user_validate::data::brand%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"title": "Reauthenticate an August account"
|
||||
}
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -199,23 +199,19 @@ class AuthProvidersView(HomeAssistantView):
|
||||
)
|
||||
|
||||
|
||||
def _prepare_result_json(
|
||||
result: AuthFlowResult,
|
||||
) -> AuthFlowResult:
|
||||
"""Convert result to JSON."""
|
||||
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
data = result.copy()
|
||||
data.pop("result")
|
||||
data.pop("data")
|
||||
return data
|
||||
return {
|
||||
key: val for key, val in result.items() if key not in ("result", "data")
|
||||
}
|
||||
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
data = dict(result)
|
||||
if (schema := result["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
@@ -268,7 +264,7 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
result.pop("data")
|
||||
result.pop("context")
|
||||
|
||||
result_obj: Credentials = result.pop("result")
|
||||
result_obj = result.pop("result")
|
||||
|
||||
# Result can be None if credential was never linked to a user before.
|
||||
user = await hass.auth.async_get_user_by_credentials(result_obj)
|
||||
@@ -281,7 +277,8 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
)
|
||||
|
||||
process_success_login(request)
|
||||
result["result"] = self._store_result(client_id, result_obj)
|
||||
# We overwrite the Credentials object with the string code to retrieve it.
|
||||
result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item]
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
@@ -149,20 +149,16 @@ def websocket_depose_mfa(
|
||||
hass.async_create_task(async_depose(msg))
|
||||
|
||||
|
||||
def _prepare_result_json(
|
||||
result: data_entry_flow.FlowResult,
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Convert result to JSON."""
|
||||
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return result.copy()
|
||||
|
||||
return dict(result)
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
data = dict(result)
|
||||
if (schema := result["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
API_ABS_HUMID = "abs_humid"
|
||||
API_CO2 = "carbon_dioxide"
|
||||
API_DEW_POINT = "dew_point"
|
||||
API_DUST = "dust"
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_SW_VERSION,
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
@@ -33,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
API_ABS_HUMID,
|
||||
API_CO2,
|
||||
API_DEW_POINT,
|
||||
API_DUST,
|
||||
@@ -120,6 +122,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AwairSensorEntityDescription(
|
||||
key=API_ABS_HUMID,
|
||||
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
|
||||
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
unique_id_tag="absolute_humidity",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==64"],
|
||||
"requirements": ["axis==65"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioazuredevops"],
|
||||
"requirements": ["aioazuredevops==2.2.1"]
|
||||
"requirements": ["aioazuredevops==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ class BackupConfigData:
|
||||
schedule=BackupSchedule(
|
||||
days=days,
|
||||
recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]),
|
||||
state=ScheduleState(data["schedule"].get("state", ScheduleState.NEVER)),
|
||||
time=time,
|
||||
),
|
||||
)
|
||||
@@ -453,7 +452,6 @@ class StoredBackupSchedule(TypedDict):
|
||||
|
||||
days: list[Day]
|
||||
recurrence: ScheduleRecurrence
|
||||
state: ScheduleState
|
||||
time: str | None
|
||||
|
||||
|
||||
@@ -462,7 +460,6 @@ class ScheduleParametersDict(TypedDict, total=False):
|
||||
|
||||
days: list[Day]
|
||||
recurrence: ScheduleRecurrence
|
||||
state: ScheduleState
|
||||
time: dt.time | None
|
||||
|
||||
|
||||
@@ -486,32 +483,12 @@ class ScheduleRecurrence(StrEnum):
|
||||
CUSTOM_DAYS = "custom_days"
|
||||
|
||||
|
||||
class ScheduleState(StrEnum):
|
||||
"""Represent the schedule recurrence.
|
||||
|
||||
This is deprecated and can be remove in HA Core 2025.8.
|
||||
"""
|
||||
|
||||
NEVER = "never"
|
||||
DAILY = "daily"
|
||||
MONDAY = "mon"
|
||||
TUESDAY = "tue"
|
||||
WEDNESDAY = "wed"
|
||||
THURSDAY = "thu"
|
||||
FRIDAY = "fri"
|
||||
SATURDAY = "sat"
|
||||
SUNDAY = "sun"
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BackupSchedule:
|
||||
"""Represent the backup schedule."""
|
||||
|
||||
days: list[Day] = field(default_factory=list)
|
||||
recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER
|
||||
# Although no longer used, state is kept for backwards compatibility.
|
||||
# It can be removed in HA Core 2025.8.
|
||||
state: ScheduleState = ScheduleState.NEVER
|
||||
time: dt.time | None = None
|
||||
cron_event: CronSim | None = field(init=False, default=None)
|
||||
next_automatic_backup: datetime | None = field(init=False, default=None)
|
||||
@@ -610,7 +587,6 @@ class BackupSchedule:
|
||||
return StoredBackupSchedule(
|
||||
days=self.days,
|
||||
recurrence=self.recurrence,
|
||||
state=self.state,
|
||||
time=self.time.isoformat() if self.time else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class BackupManager:
|
||||
)
|
||||
if unavailable_agents:
|
||||
LOGGER.warning(
|
||||
"Backup agents %s are not available, will backupp to %s",
|
||||
"Backup agents %s are not available, will backup to %s",
|
||||
unavailable_agents,
|
||||
available_agents,
|
||||
)
|
||||
|
||||
@@ -331,9 +331,6 @@ async def handle_config_info(
|
||||
"""Send the stored backup config."""
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
config = manager.config.data.to_dict()
|
||||
# Remove state from schedule, it's not needed in the frontend
|
||||
# mypy doesn't like deleting from TypedDict, ignore it
|
||||
del config["schedule"]["state"] # type: ignore[misc]
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
}
|
||||
},
|
||||
"preset1": {
|
||||
"name": "Favourite 1",
|
||||
"name": "Favorite 1",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -107,7 +107,7 @@
|
||||
}
|
||||
},
|
||||
"preset2": {
|
||||
"name": "Favourite 2",
|
||||
"name": "Favorite 2",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
},
|
||||
"preset3": {
|
||||
"name": "Favourite 3",
|
||||
"name": "Favorite 3",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
},
|
||||
"preset4": {
|
||||
"name": "Favourite 4",
|
||||
"name": "Favorite 4",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
|
||||
@@ -25,7 +25,6 @@ SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
||||
@@ -5,12 +5,12 @@ from __future__ import annotations
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_PIN
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
||||
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkConfigEntry
|
||||
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["bluecurrent_api"],
|
||||
"requirements": ["bluecurrent-api==1.2.4"]
|
||||
"requirements": ["bluecurrent-api==1.3.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.1"],
|
||||
"requirements": ["pyblu==2.0.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
@@ -388,12 +388,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||
scanner = HaScanner(mode, adapter, address)
|
||||
scanner.async_setup()
|
||||
try:
|
||||
await scanner.async_start()
|
||||
except (RuntimeError, ScannerStartError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{adapter_human_name(adapter, address)}: {err}"
|
||||
) from err
|
||||
adapters = await manager.async_get_bluetooth_adapters()
|
||||
details = adapters[adapter]
|
||||
if entry.title == address:
|
||||
@@ -401,8 +395,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, title=adapter_title(adapter, details)
|
||||
)
|
||||
slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS
|
||||
# Register the scanner before starting so
|
||||
# any raw advertisement data can be processed
|
||||
entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots))
|
||||
await async_update_device(hass, entry, adapter, details)
|
||||
try:
|
||||
await scanner.async_start()
|
||||
except (RuntimeError, ScannerStartError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"{adapter_human_name(adapter, address)}: {err}"
|
||||
) from err
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_listener))
|
||||
entry.async_on_unload(scanner.async_stop)
|
||||
return True
|
||||
|
||||
@@ -235,10 +235,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
|
||||
def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
|
||||
"""Save the scanner history."""
|
||||
if isinstance(scanner, BaseHaRemoteScanner):
|
||||
self.storage.async_set_advertisement_history(
|
||||
scanner.source, scanner.serialize_discovered_devices()
|
||||
)
|
||||
self.storage.async_set_advertisement_history(
|
||||
scanner.source, scanner.serialize_discovered_devices()
|
||||
)
|
||||
|
||||
def _async_unregister_scanner(
|
||||
self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
|
||||
@@ -285,9 +284,8 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
connection_slots: int | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register a scanner."""
|
||||
if isinstance(scanner, BaseHaRemoteScanner):
|
||||
if history := self.storage.async_get_advertisement_history(scanner.source):
|
||||
scanner.restore_discovered_devices(history)
|
||||
if history := self.storage.async_get_advertisement_history(scanner.source):
|
||||
scanner.restore_discovered_devices(history)
|
||||
|
||||
unregister = super().async_register_scanner(scanner, connection_slots)
|
||||
return partial(self._async_unregister_scanner, scanner, unregister)
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==1.0.1",
|
||||
"bleak-retry-connector==4.0.0",
|
||||
"bleak-retry-connector==4.3.0",
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.44.2",
|
||||
"habluetooth==4.0.1"
|
||||
"dbus-fast==2.44.3",
|
||||
"habluetooth==5.1.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,13 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
def serialize_service_info(
|
||||
service_info: BluetoothServiceInfoBleak, time_diff: float
|
||||
) -> dict[str, Any]:
|
||||
"""Serialize a BluetoothServiceInfoBleak object."""
|
||||
"""Serialize a BluetoothServiceInfoBleak object.
|
||||
|
||||
The raw field is included for:
|
||||
1. Debugging - to see the actual advertisement packet
|
||||
2. Data freshness - manufacturer_data and service_data are aggregated
|
||||
across multiple advertisements, raw shows the latest packet only
|
||||
"""
|
||||
return {
|
||||
"name": service_info.name,
|
||||
"address": service_info.address,
|
||||
@@ -57,6 +63,7 @@ def serialize_service_info(
|
||||
"connectable": service_info.connectable,
|
||||
"time": service_info.time + time_diff,
|
||||
"tx_power": service_info.tx_power,
|
||||
"raw": service_info.raw.hex() if service_info.raw else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ CONF_INSTALLER_CODE = "installer_code"
|
||||
CONF_USER_CODE = "user_code"
|
||||
ATTR_DATETIME = "datetime"
|
||||
SERVICE_SET_DATE_TIME = "set_date_time"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
||||
@@ -9,12 +9,13 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||
from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"name": "Battery missing"
|
||||
},
|
||||
"panel_fault_ac_fail": {
|
||||
"name": "AC Failure"
|
||||
"name": "AC failure"
|
||||
},
|
||||
"panel_fault_parameter_crc_fail_in_pif": {
|
||||
"name": "CRC failure in panel configuration"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user