mirror of
https://github.com/home-assistant/core.git
synced 2025-10-07 10:49:24 +00:00
Compare commits
1502 Commits
2025.9.0
...
trigger_ac
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6027b697c9 | ||
![]() |
7a86007083 | ||
![]() |
a6e21a54a3 | ||
![]() |
8b3cb69b79 | ||
![]() |
75e900606e | ||
![]() |
7c665c53b5 | ||
![]() |
f72047eb02 | ||
![]() |
ade424c074 | ||
![]() |
5ad805de3c | ||
![]() |
ece77cf620 | ||
![]() |
7eaa559056 | ||
![]() |
08a9377373 | ||
![]() |
a2837e6aee | ||
![]() |
fa03f6194d | ||
![]() |
d2851ea1df | ||
![]() |
72f8ac7857 | ||
![]() |
77a267bc2f | ||
![]() |
ad238daadc | ||
![]() |
42370ba203 | ||
![]() |
d9691c2a3b | ||
![]() |
66cca981a9 | ||
![]() |
9640ebb593 | ||
![]() |
645f32fd65 | ||
![]() |
cb6e65f972 | ||
![]() |
425bdc0ba6 | ||
![]() |
c36341e51f | ||
![]() |
553d896899 | ||
![]() |
ac79b3072e | ||
![]() |
c0aa9bfd4b | ||
![]() |
e97100028d | ||
![]() |
da89617432 | ||
![]() |
e6203dffd3 | ||
![]() |
c13cfe9c37 | ||
![]() |
2447df9341 | ||
![]() |
1c1fbe0ec1 | ||
![]() |
4a6d2017fd | ||
![]() |
b4997a52df | ||
![]() |
464dec1dcb | ||
![]() |
85506ac78a | ||
![]() |
6d97355b42 | ||
![]() |
f9e75c616a | ||
![]() |
a821d02dfb | ||
![]() |
e05169c7a4 | ||
![]() |
1cc3431529 | ||
![]() |
4ba765f265 | ||
![]() |
50a7af4179 | ||
![]() |
e0a2116e88 | ||
![]() |
d8e1ed5f4a | ||
![]() |
f1b8e8a963 | ||
![]() |
9a9fd44c62 | ||
![]() |
bc3fe7a18e | ||
![]() |
19f3559345 | ||
![]() |
fad0e23797 | ||
![]() |
7f931e4d70 | ||
![]() |
a04835629b | ||
![]() |
78cd80746d | ||
![]() |
9ac93920d8 | ||
![]() |
1818fce1ae | ||
![]() |
f524edc4b9 | ||
![]() |
19f990ed31 | ||
![]() |
5d83c82b81 | ||
![]() |
d63d154457 | ||
![]() |
933b15ce36 | ||
![]() |
6ec7b63ebe | ||
![]() |
26bfbc55e9 | ||
![]() |
d75ca0f5f3 | ||
![]() |
fed8f137e9 | ||
![]() |
f44d65e023 | ||
![]() |
a270bd76de | ||
![]() |
9209e419ec | ||
![]() |
98f8f15e90 | ||
![]() |
b2a2868afd | ||
![]() |
0d4737d360 | ||
![]() |
2b370a0eca | ||
![]() |
618fe81207 | ||
![]() |
c0fe4861f9 | ||
![]() |
dfd33fdab1 | ||
![]() |
cceee05c15 | ||
![]() |
f560d2a05e | ||
![]() |
3601cff88e | ||
![]() |
ca5c0a759f | ||
![]() |
6f9e6909ce | ||
![]() |
ccf563437b | ||
![]() |
78e97428fd | ||
![]() |
8b4c730993 | ||
![]() |
0a071a13e2 | ||
![]() |
ab80991eac | ||
![]() |
ee7262efb4 | ||
![]() |
ea5a52cdc8 | ||
![]() |
31fe0322ab | ||
![]() |
e8e0eabb99 | ||
![]() |
1629dad1a8 | ||
![]() |
d9baad530a | ||
![]() |
4a1d00e59a | ||
![]() |
437e4e027c | ||
![]() |
3726f7eca9 | ||
![]() |
c943cf515c | ||
![]() |
3b0c2a7e56 | ||
![]() |
6ebaa9cd1d | ||
![]() |
f81c32f6ea | ||
![]() |
c0cd7a1a62 | ||
![]() |
7a61c818c6 | ||
![]() |
2800625bcf | ||
![]() |
cfec998221 | ||
![]() |
7203cffbd7 | ||
![]() |
23397ef6a9 | ||
![]() |
0e154635ff | ||
![]() |
2e6e518722 | ||
![]() |
e0cded97c7 | ||
![]() |
87a6a029bb | ||
![]() |
1cc3c22d3f | ||
![]() |
2341d1d965 | ||
![]() |
a0bae9485c | ||
![]() |
f281b0fc6b | ||
![]() |
6f89fe81cc | ||
![]() |
34f6ead7a1 | ||
![]() |
8985527a87 | ||
![]() |
bd87a3aa4d | ||
![]() |
768a505904 | ||
![]() |
d97c1f0fc3 | ||
![]() |
c3fcd34d4c | ||
![]() |
44d9eaea95 | ||
![]() |
0f34f5139a | ||
![]() |
2afb1a673d | ||
![]() |
c2f7f29630 | ||
![]() |
b01f5dd24b | ||
![]() |
0cda0c449f | ||
![]() |
40fdf12bc9 | ||
![]() |
3939a80302 | ||
![]() |
d32a102613 | ||
![]() |
20949d39c4 | ||
![]() |
310a0c8d13 | ||
![]() |
c9e80ac7e9 | ||
![]() |
5df4e9e1cf | ||
![]() |
4022ee74e8 | ||
![]() |
80a4115c44 | ||
![]() |
ce548efd80 | ||
![]() |
2edf622b41 | ||
![]() |
66ac9078aa | ||
![]() |
ba75f18f5a | ||
![]() |
8ee2ece03e | ||
![]() |
7060ab8c44 | ||
![]() |
85d8244b8a | ||
![]() |
3f9421ab08 | ||
![]() |
2f3fbf00b7 | ||
![]() |
d595ec8a07 | ||
![]() |
4ff5462cc4 | ||
![]() |
404f95b442 | ||
![]() |
89cf784022 | ||
![]() |
02142f352d | ||
![]() |
ec3dd7d1e5 | ||
![]() |
7355799030 | ||
![]() |
982166df3c | ||
![]() |
c7d3512ad2 | ||
![]() |
ada6f7b3fb | ||
![]() |
78e16495bd | ||
![]() |
12085e6152 | ||
![]() |
6764463689 | ||
![]() |
7055276665 | ||
![]() |
71b3ebd15a | ||
![]() |
b87910e596 | ||
![]() |
e19bfd670b | ||
![]() |
7b3c96e80b | ||
![]() |
01ff3cf9d9 | ||
![]() |
d66da0c10d | ||
![]() |
3491bb1b40 | ||
![]() |
3bf995eb71 | ||
![]() |
2169ce1722 | ||
![]() |
275e9485e9 | ||
![]() |
95198ae540 | ||
![]() |
aed2d3899d | ||
![]() |
4011d62ac7 | ||
![]() |
d2aa0573de | ||
![]() |
571b2e3ab6 | ||
![]() |
a7f48360b7 | ||
![]() |
22f2f8680a | ||
![]() |
d92004a9e7 | ||
![]() |
64875894d6 | ||
![]() |
3f7a288526 | ||
![]() |
a2a067a81c | ||
![]() |
f9f61b8da7 | ||
![]() |
cd69b82fc9 | ||
![]() |
f296a215e7 | ||
![]() |
d20631598e | ||
![]() |
229ebe16f3 | ||
![]() |
a172f67d37 | ||
![]() |
4b5fd38849 | ||
![]() |
ee4a1de566 | ||
![]() |
7ab99c028c | ||
![]() |
0e1d12b1ae | ||
![]() |
308f6eb5a8 | ||
![]() |
82f1ae3519 | ||
![]() |
e090ddd761 | ||
![]() |
9721ce6877 | ||
![]() |
8dde94f421 | ||
![]() |
f5f6b22af1 | ||
![]() |
f8a93b6561 | ||
![]() |
840a03f048 | ||
![]() |
85f3b5ce78 | ||
![]() |
f4284fec2f | ||
![]() |
3a89b3152f | ||
![]() |
a0356328c3 | ||
![]() |
4b6f37b1d7 | ||
![]() |
716705fb5a | ||
![]() |
d246836480 | ||
![]() |
6ee2b82d15 | ||
![]() |
73ff8d36a5 | ||
![]() |
1397def3b8 | ||
![]() |
d443529041 | ||
![]() |
373bb20f1b | ||
![]() |
3b44cce6dc | ||
![]() |
46056fe45b | ||
![]() |
1816c190b2 | ||
![]() |
00abaee6b3 | ||
![]() |
3a301f54e0 | ||
![]() |
762accbd6d | ||
![]() |
e0422d7d34 | ||
![]() |
6ba2057a88 | ||
![]() |
752969bce5 | ||
![]() |
efbdfd2954 | ||
![]() |
bb7a177a5d | ||
![]() |
9b56ca8cde | ||
![]() |
b8660b4248 | ||
![]() |
fba50af1c3 | ||
![]() |
b0a08782e0 | ||
![]() |
6c9955f220 | ||
![]() |
f56b94c0f9 | ||
![]() |
3cf035820b | ||
![]() |
99a796d066 | ||
![]() |
1cd1b1aba8 | ||
![]() |
4131c14629 | ||
![]() |
c2acda5796 | ||
![]() |
4806e7e9d9 | ||
![]() |
76606fd44f | ||
![]() |
2983f1a3b6 | ||
![]() |
8019779b3a | ||
![]() |
62cdcbf422 | ||
![]() |
b12a5a36e1 | ||
![]() |
e32763e464 | ||
![]() |
b85cf3f9d2 | ||
![]() |
3777bcc2af | ||
![]() |
9a29cc53ef | ||
![]() |
52cde48ff0 | ||
![]() |
bf1da35303 | ||
![]() |
bdd448fbe0 | ||
![]() |
55d5e769b2 | ||
![]() |
c1bf11da34 | ||
![]() |
3c20325b37 | ||
![]() |
6cd1283b00 | ||
![]() |
dde60cdecb | ||
![]() |
f03b16bdf8 | ||
![]() |
fd8ccb8d8f | ||
![]() |
d76e947021 | ||
![]() |
c91ed96543 | ||
![]() |
b164531ba8 | ||
![]() |
7c623a8704 | ||
![]() |
7ae3340336 | ||
![]() |
653b73c601 | ||
![]() |
f616e5a4e3 | ||
![]() |
c0317f60cc | ||
![]() |
8abfe424e1 | ||
![]() |
8de200de0b | ||
![]() |
f242e294be | ||
![]() |
58cc7c8f84 | ||
![]() |
bd10f6ec08 | ||
![]() |
ed9cfb4c4b | ||
![]() |
a6b6e4c4b8 | ||
![]() |
36ff5c0d45 | ||
![]() |
de6d34fec5 | ||
![]() |
38f9067970 | ||
![]() |
53a8a250d0 | ||
![]() |
00f6d26ede | ||
![]() |
6d09411c07 | ||
![]() |
037e2bfd31 | ||
![]() |
c893552d4a | ||
![]() |
4fd10162c9 | ||
![]() |
392ee5ae7e | ||
![]() |
bf190609a0 | ||
![]() |
e982ac1e53 | ||
![]() |
b4747ea87b | ||
![]() |
df69bcecb7 | ||
![]() |
7c93d91bae | ||
![]() |
07da0cfb2b | ||
![]() |
b411a11c2c | ||
![]() |
0555b84d05 | ||
![]() |
790bddef63 | ||
![]() |
a3089b8aa7 | ||
![]() |
77c8426d63 | ||
![]() |
faf226f6c2 | ||
![]() |
06d143b81a | ||
![]() |
08b6a0a702 | ||
![]() |
a20d1e3656 | ||
![]() |
36cc3682ca | ||
![]() |
1b495ecafa | ||
![]() |
7d1a0be07e | ||
![]() |
327f65c991 | ||
![]() |
4ac89f6849 | ||
![]() |
db3b070ed0 | ||
![]() |
6d940f476a | ||
![]() |
1ca701dda4 | ||
![]() |
291c44100c | ||
![]() |
c8d676e06b | ||
![]() |
4c1ae0eddc | ||
![]() |
39eadc814f | ||
![]() |
f7ecad61ba | ||
![]() |
fa4cb54549 | ||
![]() |
2be33c5e0a | ||
![]() |
904d7e5d5a | ||
![]() |
dbc4a65d48 | ||
![]() |
b93f4aabf1 | ||
![]() |
9eaa40c7a4 | ||
![]() |
b308a882fb | ||
![]() |
7f63ba2087 | ||
![]() |
d7269cfcc6 | ||
![]() |
2850a574f6 | ||
![]() |
dcb8d4f702 | ||
![]() |
aeadc0c4b0 | ||
![]() |
683c6b17be | ||
![]() |
69dd5c91b7 | ||
![]() |
5cf7dfca8f | ||
![]() |
62a49d4244 | ||
![]() |
93ee6322f2 | ||
![]() |
914990b58a | ||
![]() |
f78bb5adb6 | ||
![]() |
905f5e7289 | ||
![]() |
ec503618c3 | ||
![]() |
7a41cbc314 | ||
![]() |
c58ba734e7 | ||
![]() |
68f63be62f | ||
![]() |
2aa4ca1351 | ||
![]() |
fbabb27787 | ||
![]() |
0960d78eb5 | ||
![]() |
474b40511f | ||
![]() |
18b80aced3 | ||
![]() |
c75dca743a | ||
![]() |
00d667ed51 | ||
![]() |
51e098e807 | ||
![]() |
5e2b27699e | ||
![]() |
be942c2888 | ||
![]() |
584c1fbd97 | ||
![]() |
abc5c6e2b4 | ||
![]() |
d9de964035 | ||
![]() |
bb02158d1a | ||
![]() |
be10f097c7 | ||
![]() |
7084bca783 | ||
![]() |
cd6f3a0fe5 | ||
![]() |
af2888331d | ||
![]() |
b92e5d7131 | ||
![]() |
f7265c85d0 | ||
![]() |
8466dbf69f | ||
![]() |
2dd0d69bcd | ||
![]() |
6783c4ad83 | ||
![]() |
07d7f4e18d | ||
![]() |
54b1749986 | ||
![]() |
eaf264361f | ||
![]() |
d8f6f17a4f | ||
![]() |
9a969cea63 | ||
![]() |
ef16327b2b | ||
![]() |
a6a6261168 | ||
![]() |
a01eb48db8 | ||
![]() |
eb103a8d9a | ||
![]() |
2b5f989855 | ||
![]() |
4e247a6ebe | ||
![]() |
b964d362b7 | ||
![]() |
3914e41f3c | ||
![]() |
82bdfcb99b | ||
![]() |
976cea600f | ||
![]() |
8c8713c3f7 | ||
![]() |
2359ae6ce7 | ||
![]() |
b570fd35c8 | ||
![]() |
9d94e6b3b4 | ||
![]() |
cfab789823 | ||
![]() |
81917425dc | ||
![]() |
bfb62709d4 | ||
![]() |
ca3f2ee782 | ||
![]() |
fc8703a40f | ||
![]() |
80517c7ac1 | ||
![]() |
2b4b46eaf8 | ||
![]() |
40b9dae608 | ||
![]() |
5975cd6e09 | ||
![]() |
258c9ff52b | ||
![]() |
89c5d498a4 | ||
![]() |
76cb4d123a | ||
![]() |
f0c29c7699 | ||
![]() |
aa4151ced7 | ||
![]() |
0a6fa978fa | ||
![]() |
dc02002b9d | ||
![]() |
f071a3f38b | ||
![]() |
b935231e47 | ||
![]() |
b9f7613567 | ||
![]() |
1289a031ab | ||
![]() |
289546ef6d | ||
![]() |
aacff4db5d | ||
![]() |
f833b56122 | ||
![]() |
7eb0f2993f | ||
![]() |
abb341abfe | ||
![]() |
0d90614369 | ||
![]() |
ec84bebeea | ||
![]() |
9176867d6b | ||
![]() |
281a137ff5 | ||
![]() |
d6543480ac | ||
![]() |
ae6391b866 | ||
![]() |
10b56e4258 | ||
![]() |
0ff2597957 | ||
![]() |
026b28e962 | ||
![]() |
9a1e67294a | ||
![]() |
c3f45d594b | ||
![]() |
cdb448a5cc | ||
![]() |
ab80e726e2 | ||
![]() |
2d5d0f67b2 | ||
![]() |
d4100b6096 | ||
![]() |
955e854d77 | ||
![]() |
0c37f88c49 | ||
![]() |
48167eeb9c | ||
![]() |
24177197f7 | ||
![]() |
863fc0ba97 | ||
![]() |
9f7b229d02 | ||
![]() |
ffd909f3d9 | ||
![]() |
1ebf096a33 | ||
![]() |
96d51965e5 | ||
![]() |
04b510b020 | ||
![]() |
c9a301d50e | ||
![]() |
b304bd1a8b | ||
![]() |
b99525b231 | ||
![]() |
634db13990 | ||
![]() |
ad51a77989 | ||
![]() |
3348a39e8a | ||
![]() |
81c2e356ec | ||
![]() |
a95af1a40e | ||
![]() |
de6c3512d2 | ||
![]() |
36dc1e938a | ||
![]() |
07a78cf6f7 | ||
![]() |
eaa673e0c3 | ||
![]() |
f2c4ca081f | ||
![]() |
e3d707f0b4 | ||
![]() |
fb93fed2e5 | ||
![]() |
95dfc2f23d | ||
![]() |
77f897a768 | ||
![]() |
4f0a6ef9a1 | ||
![]() |
408df2093a | ||
![]() |
66c6b0f5fc | ||
![]() |
dd01243391 | ||
![]() |
66c17e250a | ||
![]() |
723902e233 | ||
![]() |
59fdb9f3b5 | ||
![]() |
d83502514a | ||
![]() |
08e81b2ba6 | ||
![]() |
1e808c965d | ||
![]() |
563b58c9aa | ||
![]() |
cf223880e8 | ||
![]() |
4058ca59ed | ||
![]() |
1386c01733 | ||
![]() |
46504947f7 | ||
![]() |
0a44682014 | ||
![]() |
06a57473a9 | ||
![]() |
fbed66ef1f | ||
![]() |
99a0380ec5 | ||
![]() |
68c51dc7aa | ||
![]() |
3d945b0fc5 | ||
![]() |
7b26a93d38 | ||
![]() |
1b2eab00be | ||
![]() |
750e849f09 | ||
![]() |
f32bf0cc3e | ||
![]() |
dbbe3145b6 | ||
![]() |
f8bf3ea2ef | ||
![]() |
053bd31d43 | ||
![]() |
1aefc3f37a | ||
![]() |
3de955d9ce | ||
![]() |
0ff88fd366 | ||
![]() |
eb84020773 | ||
![]() |
4bbfea3c7c | ||
![]() |
63d4fb7558 | ||
![]() |
953895cd81 | ||
![]() |
a6c3f4efc0 | ||
![]() |
11e880d034 | ||
![]() |
e4d6bdb398 | ||
![]() |
6ced1783e3 | ||
![]() |
8051f78d10 | ||
![]() |
b724176b23 | ||
![]() |
fdca16ea92 | ||
![]() |
f8fd8b432a | ||
![]() |
9148ae70ce | ||
![]() |
447cb26d28 | ||
![]() |
2af36465f6 | ||
![]() |
d5f7265424 | ||
![]() |
cc16af7f2d | ||
![]() |
7a4d75bc44 | ||
![]() |
ec0380fd3b | ||
![]() |
b17cc71dfb | ||
![]() |
89b327ed7b | ||
![]() |
9bf361a1b8 | ||
![]() |
d11c171c75 | ||
![]() |
c523c45d17 | ||
![]() |
c1b9c0e1b6 | ||
![]() |
487b9ff03e | ||
![]() |
ec62b0cdfb | ||
![]() |
6d0470064f | ||
![]() |
7450b3fd1a | ||
![]() |
5b70910d77 | ||
![]() |
6aaddad56b | ||
![]() |
a5af974209 | ||
![]() |
09e45f6f54 | ||
![]() |
d857d8850c | ||
![]() |
ccc50f2412 | ||
![]() |
3905723900 | ||
![]() |
cee88473a2 | ||
![]() |
cdf613d3f8 | ||
![]() |
52de5ff5ff | ||
![]() |
c4389a1679 | ||
![]() |
35faaa6cae | ||
![]() |
3c0b13975a | ||
![]() |
bc88696339 | ||
![]() |
8f99c3f64a | ||
![]() |
88016d96d4 | ||
![]() |
47df73b18f | ||
![]() |
1c12d2b8cd | ||
![]() |
eb38837a8c | ||
![]() |
159c7fbfd1 | ||
![]() |
7ee31f0884 | ||
![]() |
0c5e12571a | ||
![]() |
9db973217f | ||
![]() |
fa863649fa | ||
![]() |
b7c6e21707 | ||
![]() |
cf1a745283 | ||
![]() |
834e3f1963 | ||
![]() |
3f8f7573c9 | ||
![]() |
156a0f1a3d | ||
![]() |
cc2a5b43dd | ||
![]() |
0ae272f1f6 | ||
![]() |
8774295e2e | ||
![]() |
731064f7e9 | ||
![]() |
2f75661c20 | ||
![]() |
be6f056f30 | ||
![]() |
79599e1284 | ||
![]() |
a255585ab6 | ||
![]() |
e9bde225fe | ||
![]() |
d9521ac2a0 | ||
![]() |
d8b24ccccd | ||
![]() |
b4417a76d5 | ||
![]() |
274f6eb54a | ||
![]() |
21a5aaf35c | ||
![]() |
05820a49d0 | ||
![]() |
0c8d2594ef | ||
![]() |
205bd2676b | ||
![]() |
25849fd9cc | ||
![]() |
7d6eac9ff7 | ||
![]() |
31017ebc98 | ||
![]() |
724a7b0ecc | ||
![]() |
91e13d447a | ||
![]() |
7c8ad9d535 | ||
![]() |
9cd3ab853d | ||
![]() |
0b0f8c5829 | ||
![]() |
ae7bc7fb1b | ||
![]() |
09750872b5 | ||
![]() |
076e51017b | ||
![]() |
95e7b00996 | ||
![]() |
e7da1250ba | ||
![]() |
ddecf1ac21 | ||
![]() |
17b12d29af | ||
![]() |
9cc78680d6 | ||
![]() |
14d42e43bf | ||
![]() |
ed5f5d4b33 | ||
![]() |
c3ba086fad | ||
![]() |
7b5314605c | ||
![]() |
3a806d6603 | ||
![]() |
e71140e09b | ||
![]() |
6dd33f900d | ||
![]() |
2844bd474a | ||
![]() |
d865fcf999 | ||
![]() |
79a2fc5a01 | ||
![]() |
19d87abb8a | ||
![]() |
c4de46a85b | ||
![]() |
e79a434d9b | ||
![]() |
9a801424c7 | ||
![]() |
5cb186980a | ||
![]() |
1629ade97f | ||
![]() |
ccf0011ac2 | ||
![]() |
70077511a3 | ||
![]() |
dfbaf66021 | ||
![]() |
62cea48a58 | ||
![]() |
c493c7dd67 | ||
![]() |
53875f7188 | ||
![]() |
526541f666 | ||
![]() |
fdaceaddfd | ||
![]() |
a2f4073d54 | ||
![]() |
2d01a99ec2 | ||
![]() |
311d4c4262 | ||
![]() |
e14f5ba44d | ||
![]() |
9babc85517 | ||
![]() |
332a3fad3c | ||
![]() |
8782aa4f60 | ||
![]() |
475b84cc5f | ||
![]() |
0f904d418b | ||
![]() |
4ea4eec2d8 | ||
![]() |
afefa16615 | ||
![]() |
1dccbee45c | ||
![]() |
711a56db2f | ||
![]() |
9d1c7dadff | ||
![]() |
7d1953e387 | ||
![]() |
023ecf2a64 | ||
![]() |
934db458a3 | ||
![]() |
0a6ae3b52a | ||
![]() |
bdd0b74d51 | ||
![]() |
8837f2aca7 | ||
![]() |
403cd2d8ef | ||
![]() |
ddfc528d63 | ||
![]() |
ddea2206c3 | ||
![]() |
32aacac550 | ||
![]() |
dadba274aa | ||
![]() |
14b5b9742c | ||
![]() |
a0be737925 | ||
![]() |
ff47839c61 | ||
![]() |
9ba7dda864 | ||
![]() |
911f901d9d | ||
![]() |
2008a73657 | ||
![]() |
60bf298ca6 | ||
![]() |
3bc2ea7b5f | ||
![]() |
3bac6b86df | ||
![]() |
20293e2a11 | ||
![]() |
15cc28e6c1 | ||
![]() |
874ca1323b | ||
![]() |
ca186925af | ||
![]() |
2ab051b716 | ||
![]() |
a2a726de34 | ||
![]() |
5d543d2185 | ||
![]() |
a78c909b34 | ||
![]() |
f00ab80d17 | ||
![]() |
014881d985 | ||
![]() |
29a42a8e58 | ||
![]() |
3f70084d7f | ||
![]() |
01d81f8980 | ||
![]() |
b1ae9c95c9 | ||
![]() |
8be79ecdb0 | ||
![]() |
f6b8aa893b | ||
![]() |
c867026bdd | ||
![]() |
da3a164e66 | ||
![]() |
32688e1108 | ||
![]() |
4305ea9b4c | ||
![]() |
61153ec456 | ||
![]() |
9e4a2d5fa9 | ||
![]() |
72e608918b | ||
![]() |
86db60c442 | ||
![]() |
25806615a9 | ||
![]() |
a0f67381e5 | ||
![]() |
90bfadda9b | ||
![]() |
0f8e700965 | ||
![]() |
21d4ed2837 | ||
![]() |
ce363b3835 | ||
![]() |
dd3e6b8df5 | ||
![]() |
abbf8390ac | ||
![]() |
689039959c | ||
![]() |
52c25cfc88 | ||
![]() |
00b2017767 | ||
![]() |
dd7f7be6ad | ||
![]() |
22709506c6 | ||
![]() |
f0c0492375 | ||
![]() |
58459cb80f | ||
![]() |
a19e378447 | ||
![]() |
38a5a3ed4b | ||
![]() |
e76bed4a83 | ||
![]() |
d73309ba60 | ||
![]() |
19fdea024c | ||
![]() |
a3cfd7f707 | ||
![]() |
3dd941eff7 | ||
![]() |
d389141aee | ||
![]() |
3c542b8d43 | ||
![]() |
2367df89d9 | ||
![]() |
7bfdfb3fc7 | ||
![]() |
485916265a | ||
![]() |
1bb3c96fc1 | ||
![]() |
4eaf6784af | ||
![]() |
7d96a814f9 | ||
![]() |
7b7265a6b0 | ||
![]() |
9059e3dadc | ||
![]() |
d9d42b3ad5 | ||
![]() |
d565fb3cb4 | ||
![]() |
6e93e480d1 | ||
![]() |
5a3570702d | ||
![]() |
b26b1df143 | ||
![]() |
fdbff76733 | ||
![]() |
018d59a892 | ||
![]() |
4b6dd0eb8f | ||
![]() |
b7db87bd3d | ||
![]() |
86dc453c55 | ||
![]() |
a4f2c88c7f | ||
![]() |
3cdb894e61 | ||
![]() |
cb837aaae5 | ||
![]() |
82443ded34 | ||
![]() |
71cc3b7fcd | ||
![]() |
e5658f9747 | ||
![]() |
868ded141f | ||
![]() |
1151fa698d | ||
![]() |
2796d6110a | ||
![]() |
844b97bd32 | ||
![]() |
286b2500bd | ||
![]() |
4b7746ab51 | ||
![]() |
ca1c366f4f | ||
![]() |
de42ac14ac | ||
![]() |
7f7bd5a97f | ||
![]() |
8a70a1badb | ||
![]() |
181741cab6 | ||
![]() |
1e14fb6dab | ||
![]() |
2b6a125927 | ||
![]() |
e61ad10708 | ||
![]() |
5177f9e8c2 | ||
![]() |
850aeeb5eb | ||
![]() |
a1b9061060 | ||
![]() |
0ec1f27489 | ||
![]() |
befc93bc73 | ||
![]() |
1526d953bf | ||
![]() |
d38082a5c8 | ||
![]() |
42850421d2 | ||
![]() |
21a835c4b4 | ||
![]() |
e9294dbf72 | ||
![]() |
5c4dfbff1b | ||
![]() |
abe628506d | ||
![]() |
12cc0ed18d | ||
![]() |
8ca7562390 | ||
![]() |
942f7eebb1 | ||
![]() |
1a167e6aee | ||
![]() |
9531ae10f2 | ||
![]() |
bfc9616abf | ||
![]() |
054a5d751a | ||
![]() |
a43ba4f966 | ||
![]() |
1a5cae125f | ||
![]() |
f3b9bda876 | ||
![]() |
3f3aaa2815 | ||
![]() |
6dc7870779 | ||
![]() |
be83416c72 | ||
![]() |
c745ee18eb | ||
![]() |
cf907ae196 | ||
![]() |
8eee53036a | ||
![]() |
b37237d24b | ||
![]() |
950e758b62 | ||
![]() |
9cd940b7df | ||
![]() |
10b186a20d | ||
![]() |
757aec1c6b | ||
![]() |
0b159bdb9c | ||
![]() |
8728312e87 | ||
![]() |
bbb67db354 | ||
![]() |
265f5da21a | ||
![]() |
54859e8a83 | ||
![]() |
c87dba878d | ||
![]() |
8d8e008123 | ||
![]() |
b30667a469 | ||
![]() |
8920c548d5 | ||
![]() |
eac719f9af | ||
![]() |
71c274cb91 | ||
![]() |
d4902361e6 | ||
![]() |
f63eee3889 | ||
![]() |
21bfe610d1 | ||
![]() |
21c174e895 | ||
![]() |
ec148e0459 | ||
![]() |
286763b998 | ||
![]() |
5f88122a2b | ||
![]() |
31968d16ab | ||
![]() |
c125554817 | ||
![]() |
10f2955d34 | ||
![]() |
55712b784c | ||
![]() |
fe3a929556 | ||
![]() |
534801e80d | ||
![]() |
8aeda5a0c0 | ||
![]() |
eb1cbbc75c | ||
![]() |
fa8a4d7098 | ||
![]() |
2623ebac4d | ||
![]() |
1746c51ce4 | ||
![]() |
8b984a2105 | ||
![]() |
ebee370a56 | ||
![]() |
dabd096587 | ||
![]() |
21399818af | ||
![]() |
4354214fbf | ||
![]() |
5bd39804f1 | ||
![]() |
6d3ad3ab9c | ||
![]() |
4c212bdcd4 | ||
![]() |
b91b39580f | ||
![]() |
472d70b6c9 | ||
![]() |
017a84a859 | ||
![]() |
d184540967 | ||
![]() |
1740984b3b | ||
![]() |
4db8592c61 | ||
![]() |
27e630c107 | ||
![]() |
ea8833342d | ||
![]() |
87be2ba823 | ||
![]() |
51c35eb631 | ||
![]() |
24a86d042f | ||
![]() |
cd6f653123 | ||
![]() |
fd05ddca28 | ||
![]() |
a1f2eb44ae | ||
![]() |
c4ddc03dbc | ||
![]() |
9db5aafb71 | ||
![]() |
64cdcfb613 | ||
![]() |
c761ce699c | ||
![]() |
40ebce4ae8 | ||
![]() |
29914d6722 | ||
![]() |
5eef6edded | ||
![]() |
db729273a5 | ||
![]() |
946d75d651 | ||
![]() |
093f779edb | ||
![]() |
87658e77a7 | ||
![]() |
38f65cda98 | ||
![]() |
797c6ddedd | ||
![]() |
fe8a53407a | ||
![]() |
ae5f57fd99 | ||
![]() |
a93c3cc23c | ||
![]() |
804b42e1fb | ||
![]() |
a4f15e4840 | ||
![]() |
2471177c84 | ||
![]() |
a494d3ec69 | ||
![]() |
b10a9721a7 | ||
![]() |
04c0bb20d6 | ||
![]() |
1598c4ebe8 | ||
![]() |
d67ec7593a | ||
![]() |
4a4c124181 | ||
![]() |
c34af4be86 | ||
![]() |
823071b722 | ||
![]() |
462fa77ba1 | ||
![]() |
24fc8b9297 | ||
![]() |
2596ab2940 | ||
![]() |
23fa84e20e | ||
![]() |
7f13141297 | ||
![]() |
770f41d079 | ||
![]() |
df16e85359 | ||
![]() |
3c6db923a3 | ||
![]() |
450c47f932 | ||
![]() |
048f64eccf | ||
![]() |
c4c523e8b7 | ||
![]() |
87e30e0907 | ||
![]() |
74660da2d2 | ||
![]() |
6b8c180509 | ||
![]() |
eb4a873c43 | ||
![]() |
6aafa666d6 | ||
![]() |
9ee9bb368d | ||
![]() |
6e4258c8a9 | ||
![]() |
d65e704823 | ||
![]() |
aadaf87c16 | ||
![]() |
e70b147c0c | ||
![]() |
031b12752f | ||
![]() |
df0cfd69a9 | ||
![]() |
b2c53f2d78 | ||
![]() |
3649e949b1 | ||
![]() |
de7e2303a7 | ||
![]() |
892f3f267b | ||
![]() |
0254285285 | ||
![]() |
44a95242dc | ||
![]() |
f9b1c52d65 | ||
![]() |
aa8d78622c | ||
![]() |
ca6289a576 | ||
![]() |
0f372f4b47 | ||
![]() |
4bba167ab3 | ||
![]() |
962c0c443d | ||
![]() |
c6b4cac28a | ||
![]() |
3c7e3a5e30 | ||
![]() |
fa698956c3 | ||
![]() |
32f136b12f | ||
![]() |
e1f617df25 | ||
![]() |
84f1b8a5cc | ||
![]() |
e9cedf4852 | ||
![]() |
9c72b40ab4 | ||
![]() |
65f655e5f5 | ||
![]() |
af28573894 | ||
![]() |
c5fc1de3df | ||
![]() |
1df1144eb9 | ||
![]() |
d51c0e3752 | ||
![]() |
f5157878c2 | ||
![]() |
fb723571b6 | ||
![]() |
dbf80c3ce3 | ||
![]() |
e0a774b598 | ||
![]() |
168afc5f0e | ||
![]() |
af23670854 | ||
![]() |
935ce421df | ||
![]() |
c60ad8179d | ||
![]() |
14ad3364e3 | ||
![]() |
e229f36648 | ||
![]() |
f4f99e015c | ||
![]() |
5dc509cba0 | ||
![]() |
75597ac98d | ||
![]() |
b503f792b5 | ||
![]() |
410c3df6dd | ||
![]() |
f1bf28df18 | ||
![]() |
99fb64af9b | ||
![]() |
c0af0159e3 | ||
![]() |
71749da3a3 | ||
![]() |
b01be94034 | ||
![]() |
47ec8b7f12 | ||
![]() |
93ec9e448e | ||
![]() |
90bc41dd02 | ||
![]() |
410d869f3d | ||
![]() |
d75d9f2589 | ||
![]() |
afbb832a57 | ||
![]() |
bdc881c87a | ||
![]() |
22ea269ed8 | ||
![]() |
10fecbaf4d | ||
![]() |
cbdc1dc5b6 | ||
![]() |
b203a831c9 | ||
![]() |
5ccbee4c9a | ||
![]() |
1483c9488f | ||
![]() |
f5535db24c | ||
![]() |
e40ecdfb00 | ||
![]() |
2f4c69bbd5 | ||
![]() |
dd0f6a702b | ||
![]() |
5ba580bc25 | ||
![]() |
c13002bdd5 | ||
![]() |
75d22191a0 | ||
![]() |
58d6549f1c | ||
![]() |
1fcc6df1fd | ||
![]() |
9bf467e6d1 | ||
![]() |
d877d6d93f | ||
![]() |
d2b255ba92 | ||
![]() |
1509c429d6 | ||
![]() |
af9717c1cd | ||
![]() |
49e75c9cf8 | ||
![]() |
c97f16a96d | ||
![]() |
a3a4433d62 | ||
![]() |
f832002afd | ||
![]() |
dbc7f2b43c | ||
![]() |
1cd3a1eede | ||
![]() |
7d6e0d44b0 | ||
![]() |
2bb6d745ca | ||
![]() |
beb9d7856c | ||
![]() |
6a4c8a550a | ||
![]() |
7d23752a3f | ||
![]() |
c2b2a78db5 | ||
![]() |
0fb6bbee59 | ||
![]() |
d93e0a105a | ||
![]() |
ab1619c0b4 | ||
![]() |
70df7b8503 | ||
![]() |
0e2c2ad355 | ||
![]() |
4c26718739 | ||
![]() |
96034e1525 | ||
![]() |
df1302fc1c | ||
![]() |
5a5b639aa4 | ||
![]() |
e9fbe2227f | ||
![]() |
82b57568a0 | ||
![]() |
be692ab2fd | ||
![]() |
24c04cceee | ||
![]() |
97077898bb | ||
![]() |
08485f4e09 | ||
![]() |
b64d60fce4 | ||
![]() |
3690497e1f | ||
![]() |
3499ed7a98 | ||
![]() |
2c809d5903 | ||
![]() |
40988198f3 | ||
![]() |
b87e581cde | ||
![]() |
f1c55ee7e2 | ||
![]() |
9f17a82acf | ||
![]() |
3955391cda | ||
![]() |
d9a757c7e6 | ||
![]() |
aa1ec944c0 | ||
![]() |
88c3b6a9f5 | ||
![]() |
ada73953f6 | ||
![]() |
42e9b9a0bc | ||
![]() |
ec6a052ff5 | ||
![]() |
c91d64e04d | ||
![]() |
ab5d1d27f1 | ||
![]() |
1c10b85fed | ||
![]() |
91a7db08ff | ||
![]() |
a764d54123 | ||
![]() |
dc09e33556 | ||
![]() |
14173bd9ec | ||
![]() |
d2e7537629 | ||
![]() |
9a165a64fe | ||
![]() |
9c749a6abc | ||
![]() |
2e33222c71 | ||
![]() |
ab1c2c4f70 | ||
![]() |
529219ae69 | ||
![]() |
d6ce71fa61 | ||
![]() |
e5b67d513a | ||
![]() |
a547179f66 | ||
![]() |
8c61788a7d | ||
![]() |
6b934d94db | ||
![]() |
d30ad82774 | ||
![]() |
4618b33e93 | ||
![]() |
d6299094db | ||
![]() |
087d9d30c0 | ||
![]() |
f07890cf5c | ||
![]() |
e5b78cc481 | ||
![]() |
12b409d8e1 | ||
![]() |
def5408db8 | ||
![]() |
f105b45ee2 | ||
![]() |
9d904c30a7 | ||
![]() |
99b047939f | ||
![]() |
3a615908ee | ||
![]() |
baff541f46 | ||
![]() |
6d8c35cfe9 | ||
![]() |
b8d9883e74 | ||
![]() |
c3c65af450 | ||
![]() |
3af8616764 | ||
![]() |
64ec4609c5 | ||
![]() |
c78bc26b83 | ||
![]() |
0c093646c9 | ||
![]() |
1b27acdde0 | ||
![]() |
9dafc0e02f | ||
![]() |
0091dafcb0 | ||
![]() |
b387acffb7 | ||
![]() |
36b3133fa2 | ||
![]() |
fe01e96012 | ||
![]() |
0b56ec16ed | ||
![]() |
ca79f4c963 | ||
![]() |
9a43f2776d | ||
![]() |
0ac7cb311d | ||
![]() |
3472020812 | ||
![]() |
dcd09523a6 | ||
![]() |
a5bfdc697b | ||
![]() |
dbb29a7c7d | ||
![]() |
124a63d846 | ||
![]() |
3de701a9ab | ||
![]() |
bfe1dd65b3 | ||
![]() |
71bf5e14cc | ||
![]() |
6d231c2c99 | ||
![]() |
b93072865b | ||
![]() |
14ebb6cd74 | ||
![]() |
2ddbcd560e | ||
![]() |
c5ff7ed1c9 | ||
![]() |
c4bea5616c | ||
![]() |
17fe147726 | ||
![]() |
9fae4e7e1f | ||
![]() |
0cebca498c | ||
![]() |
521ff62aae | ||
![]() |
fd1df5ad88 | ||
![]() |
91e7a35a07 | ||
![]() |
09381abf46 | ||
![]() |
3713c03c07 | ||
![]() |
bd8ddd7cd8 | ||
![]() |
f0dc1f927b | ||
![]() |
984590c6d1 | ||
![]() |
d324021a3f | ||
![]() |
1f4c0b3e9b | ||
![]() |
69893aba4b | ||
![]() |
b9dcf89b37 | ||
![]() |
54fd55a1c6 | ||
![]() |
cc64fa639d | ||
![]() |
84140ba414 | ||
![]() |
d1726b84c8 | ||
![]() |
4724ecbc38 | ||
![]() |
85afe87b5e | ||
![]() |
5960179844 | ||
![]() |
9f8f7d2fde | ||
![]() |
4c22264b13 | ||
![]() |
baf4382724 | ||
![]() |
8263ea4a4a | ||
![]() |
8412581be4 | ||
![]() |
207c848438 | ||
![]() |
2b61601fd7 | ||
![]() |
ee506e6c14 | ||
![]() |
8003a49571 | ||
![]() |
e438b11afb | ||
![]() |
64ba43703c | ||
![]() |
1d214ae120 | ||
![]() |
68d987f866 | ||
![]() |
299cc5e40c | ||
![]() |
2c3456177e | ||
![]() |
1ef90180cc | ||
![]() |
4c1364dfd1 | ||
![]() |
09a44a6a30 | ||
![]() |
63303bdcde | ||
![]() |
59cd24f54b | ||
![]() |
82b9fead39 | ||
![]() |
a879e36e9b | ||
![]() |
b12c458188 | ||
![]() |
4985f9a5a1 | ||
![]() |
ae70ca7cba | ||
![]() |
66d1cf8af7 | ||
![]() |
27c0df3da8 | ||
![]() |
5413131885 | ||
![]() |
0acd77e60a | ||
![]() |
c5d552dc4a | ||
![]() |
4f045b45ac | ||
![]() |
4ad29161bd | ||
![]() |
fea7f537a8 | ||
![]() |
442b6e9cca | ||
![]() |
531b67101d | ||
![]() |
596a3fc879 | ||
![]() |
0e8295604e | ||
![]() |
9cfdb99e76 | ||
![]() |
b1a6e403fb | ||
![]() |
937d3e4a96 | ||
![]() |
a368ad4ab5 | ||
![]() |
254694b024 | ||
![]() |
bcfa7a7383 | ||
![]() |
ab7081d26a | ||
![]() |
4762c64c25 | ||
![]() |
1b99ffe61b | ||
![]() |
d5132e8ea9 | ||
![]() |
1bcf3cfbb2 | ||
![]() |
d4d912ef55 | ||
![]() |
393826635b | ||
![]() |
9edd5c35e0 | ||
![]() |
e8c1d3dc3c | ||
![]() |
46463ea4f8 | ||
![]() |
88e6b0c8d9 | ||
![]() |
2ed92c720f | ||
![]() |
7389f23d9a | ||
![]() |
a0cef80cf2 | ||
![]() |
343b17788f | ||
![]() |
50349e49f1 | ||
![]() |
42d0415a86 | ||
![]() |
1428b41a25 | ||
![]() |
e65b4292b2 | ||
![]() |
2fc2bb97fc | ||
![]() |
40da606177 | ||
![]() |
d613b69e4e | ||
![]() |
3c5d09e114 | ||
![]() |
9c54cc369b | ||
![]() |
f91e4090f9 | ||
![]() |
2cdf0b74d5 | ||
![]() |
86750ae5c3 | ||
![]() |
c1eb492616 | ||
![]() |
ac154c020c | ||
![]() |
0e23eb9ebd | ||
![]() |
8367930f42 | ||
![]() |
d71b1246cf | ||
![]() |
4ad664a652 | ||
![]() |
002493c3e1 | ||
![]() |
720ecde568 | ||
![]() |
4c548830b4 | ||
![]() |
214925e10a | ||
![]() |
885256299f | ||
![]() |
e73c670025 | ||
![]() |
e3c0cfd1e2 | ||
![]() |
46c38f185c | ||
![]() |
9082637133 | ||
![]() |
0a35fd0ea4 | ||
![]() |
c4649fc068 | ||
![]() |
b496637bdd | ||
![]() |
7d471f9624 | ||
![]() |
83f3b3e3eb | ||
![]() |
4592d6370a | ||
![]() |
ccef31a37a | ||
![]() |
6a482b1a3e | ||
![]() |
6f00f8a920 | ||
![]() |
ceeeb22040 | ||
![]() |
2cda0817b2 | ||
![]() |
881a0bd1fa | ||
![]() |
dba6f419c9 | ||
![]() |
fad8d4fca2 | ||
![]() |
1663ad1adb | ||
![]() |
6a8152bc7f | ||
![]() |
e5edccd56f | ||
![]() |
480527eb68 | ||
![]() |
1475108f1c | ||
![]() |
a1e68336fc | ||
![]() |
07392e3ff7 | ||
![]() |
3e39f77e92 | ||
![]() |
a12617645b | ||
![]() |
723476457e | ||
![]() |
7053727426 | ||
![]() |
0f4ce58f28 | ||
![]() |
2c72cd3832 | ||
![]() |
e8d5615e54 | ||
![]() |
7a332d489d | ||
![]() |
504421e257 | ||
![]() |
a0ace3b082 | ||
![]() |
aea055b444 | ||
![]() |
5b107349a1 | ||
![]() |
46fa98e0b2 | ||
![]() |
96e66009e5 | ||
![]() |
ad14a66187 | ||
![]() |
777ac97acb | ||
![]() |
af07ab4752 | ||
![]() |
74b731528d | ||
![]() |
c361c32407 | ||
![]() |
75c1eddaf9 | ||
![]() |
715aba3aca | ||
![]() |
285619e913 | ||
![]() |
eaf400f3b7 | ||
![]() |
3d79a73110 | ||
![]() |
6271765eaf | ||
![]() |
9e73ff06d2 | ||
![]() |
36edfd8c04 | ||
![]() |
a750cfcac6 | ||
![]() |
026f20932a | ||
![]() |
07d4e11c30 | ||
![]() |
9c80d75588 | ||
![]() |
1818a103b6 | ||
![]() |
3e4bb4eb7e | ||
![]() |
1117b92dde | ||
![]() |
f5a1523068 | ||
![]() |
7005a70a4e | ||
![]() |
9b862a8e4e | ||
![]() |
f5dba77636 | ||
![]() |
da7f9f6154 | ||
![]() |
9a92d58613 | ||
![]() |
9ea438024d | ||
![]() |
58edc3742a | ||
![]() |
3c0580880d | ||
![]() |
04b5eb7d53 | ||
![]() |
cecae10a15 | ||
![]() |
2e2b9483df | ||
![]() |
0444467858 | ||
![]() |
d990c2bee2 | ||
![]() |
03a7052151 | ||
![]() |
4025e23c67 | ||
![]() |
82c3fcccc9 | ||
![]() |
7ee7a3c0b5 | ||
![]() |
e7cb0173b0 | ||
![]() |
dbdbf1cf16 | ||
![]() |
b5704f3e8b | ||
![]() |
df3d4b5db1 | ||
![]() |
0ab232b904 | ||
![]() |
acc75e4419 | ||
![]() |
8aa672882a | ||
![]() |
3cbf3bdf4c | ||
![]() |
56c865dcfe | ||
![]() |
b7360dfad8 | ||
![]() |
f2204e97ab | ||
![]() |
98df5f5f0c | ||
![]() |
064d43480d | ||
![]() |
1536375e82 | ||
![]() |
39e9ffff29 | ||
![]() |
12f152d6e4 | ||
![]() |
c7f0560208 | ||
![]() |
65603a3829 | ||
![]() |
38ea5c6813 | ||
![]() |
5b1fd8f58b | ||
![]() |
e5f99a617f | ||
![]() |
7f8b5f2288 | ||
![]() |
75f69cd5b6 | ||
![]() |
d7fab27351 | ||
![]() |
6c6ec7534f | ||
![]() |
8e3780264a | ||
![]() |
78b009dd8f | ||
![]() |
76d72ad280 | ||
![]() |
e5be9426a4 | ||
![]() |
0922f12ec0 | ||
![]() |
89f424e1d3 | ||
![]() |
143eb20d99 | ||
![]() |
3187506eb9 | ||
![]() |
a328b23437 | ||
![]() |
7e6a949559 | ||
![]() |
da7db5e22b | ||
![]() |
ec58943c8c | ||
![]() |
61a05490e9 | ||
![]() |
6a1629d2ed | ||
![]() |
106e1ce224 | ||
![]() |
601d63e3b7 | ||
![]() |
6a5f5b9adc | ||
![]() |
8ecf5a98a5 | ||
![]() |
1728c577f7 | ||
![]() |
1006d5e0ba | ||
![]() |
6c29d5dc49 | ||
![]() |
a4e086f0d9 | ||
![]() |
0fecf012e6 | ||
![]() |
63c8bfaa9b | ||
![]() |
435926fd41 | ||
![]() |
0cda883b56 | ||
![]() |
c621f0c139 | ||
![]() |
fa9007777d | ||
![]() |
ae58e633f0 | ||
![]() |
06480bfd9d | ||
![]() |
e1afadb28c | ||
![]() |
625f586945 | ||
![]() |
7dbeaa475d | ||
![]() |
dff3d5f8af | ||
![]() |
89c335919a | ||
![]() |
2bb4573357 | ||
![]() |
7037ce989c | ||
![]() |
bfdd2053ba | ||
![]() |
fcc3f92f8c | ||
![]() |
8710267d53 | ||
![]() |
85b6adcc9a | ||
![]() |
beec6e86e0 | ||
![]() |
3dacffaaf9 | ||
![]() |
d90f2a1de1 | ||
![]() |
b6c9217429 | ||
![]() |
7fc8da6769 | ||
![]() |
34c45eae56 | ||
![]() |
71b8da6497 | ||
![]() |
caa0e357ee | ||
![]() |
0721ac6c73 | ||
![]() |
783c742e09 | ||
![]() |
d2324086af | ||
![]() |
2ffd5f4c97 | ||
![]() |
aa4a110923 | ||
![]() |
0a3032e766 | ||
![]() |
c4db422355 | ||
![]() |
f4e0b9ba15 | ||
![]() |
f3b997720d | ||
![]() |
f5d3a89f90 | ||
![]() |
b90296d853 | ||
![]() |
c6d6349908 | ||
![]() |
2be6f17505 | ||
![]() |
4c953f36c8 | ||
![]() |
ec2fa202e9 | ||
![]() |
1a970e6c88 | ||
![]() |
b9db828df3 | ||
![]() |
50c0f41e8f | ||
![]() |
8cc66ee96c | ||
![]() |
71981975a4 | ||
![]() |
b875af9667 | ||
![]() |
89cd55c878 | ||
![]() |
b25708cec2 | ||
![]() |
bb9c65bc4b | ||
![]() |
447c7b64a9 | ||
![]() |
b111a33b8c | ||
![]() |
4fcd02bc5d | ||
![]() |
80d26b8d2e | ||
![]() |
a475ecb342 | ||
![]() |
42aec9cd91 | ||
![]() |
5409181b79 | ||
![]() |
c1945211fa | ||
![]() |
29537dc87d | ||
![]() |
72e1a8f912 | ||
![]() |
b742e4898c | ||
![]() |
e5565c75f6 | ||
![]() |
a7ca618327 | ||
![]() |
6cbb881647 | ||
![]() |
eae1fe4a56 | ||
![]() |
8d945d89de | ||
![]() |
86e7f3713f | ||
![]() |
3bc772a196 | ||
![]() |
c2290d6edb | ||
![]() |
2a458dcec9 | ||
![]() |
ab5ef3674f | ||
![]() |
f28251bc76 | ||
![]() |
1cca65b5c5 | ||
![]() |
ed134e22f9 | ||
![]() |
300c582ea0 | ||
![]() |
52f7e20b5c | ||
![]() |
813098cb1a | ||
![]() |
000df08bca | ||
![]() |
9b80cf7d94 | ||
![]() |
3385151c26 | ||
![]() |
111fa78c57 | ||
![]() |
e67df73c4e | ||
![]() |
b9f24bbb2a | ||
![]() |
18ca9590f0 | ||
![]() |
eccadd4a11 | ||
![]() |
aeff62faea | ||
![]() |
e5a44e5966 | ||
![]() |
1369a98fa3 | ||
![]() |
0e1dd04083 | ||
![]() |
df46816b2f | ||
![]() |
955ef3b5e7 | ||
![]() |
5fc6fb9cf3 | ||
![]() |
9ee9e1775d | ||
![]() |
712c9b9edc | ||
![]() |
d571857770 | ||
![]() |
de90922297 | ||
![]() |
e0b3a5337c | ||
![]() |
215603fae1 | ||
![]() |
1a12c619e9 | ||
![]() |
34c061df19 | ||
![]() |
c12b638b3d | ||
![]() |
b9427deed2 | ||
![]() |
d66016588b | ||
![]() |
229d0bdc77 | ||
![]() |
078425918e | ||
![]() |
da2f154111 | ||
![]() |
270a9a5a98 | ||
![]() |
9f953c2e35 | ||
![]() |
8f16b09751 | ||
![]() |
73ab041051 | ||
![]() |
4bb76c6d94 | ||
![]() |
7378d3607c | ||
![]() |
7d1e36af7f | ||
![]() |
8b03a23ed8 | ||
![]() |
a023dfc013 | ||
![]() |
fa0f707872 | ||
![]() |
a8f56e4b96 | ||
![]() |
61c904d225 | ||
![]() |
a8ff14ecb8 | ||
![]() |
3909906823 | ||
![]() |
e0bf7749e6 | ||
![]() |
72128e9708 | ||
![]() |
1b9acdc233 | ||
![]() |
0f530485d1 | ||
![]() |
0928e9a6ee | ||
![]() |
75d792207a | ||
![]() |
ceda62f6ea | ||
![]() |
12ab84a5d9 | ||
![]() |
8e85faf997 | ||
![]() |
b514a14c10 | ||
![]() |
6b609b019e | ||
![]() |
10baae92a0 | ||
![]() |
8e1ee32190 | ||
![]() |
814b98c2a3 | ||
![]() |
243569f6b8 | ||
![]() |
4b7817f1df | ||
![]() |
180f898bfa | ||
![]() |
e9dcde1bb5 | ||
![]() |
f44b6a3a39 | ||
![]() |
9b6b8003ec | ||
![]() |
2503157282 | ||
![]() |
7b2b3e9e33 | ||
![]() |
2d5f228308 | ||
![]() |
19f36fc630 | ||
![]() |
6b6553dae3 | ||
![]() |
0865d3f749 | ||
![]() |
095f73d84f | ||
![]() |
3b60961f02 | ||
![]() |
0d9079ea72 | ||
![]() |
f17db80428 | ||
![]() |
2d4b2e822a | ||
![]() |
b08a72a53d | ||
![]() |
1e4fa40a77 | ||
![]() |
ac0ff96f26 | ||
![]() |
2e50cee555 | ||
![]() |
51c6c1b0d2 | ||
![]() |
c4fce1c793 | ||
![]() |
581f8a9378 | ||
![]() |
d7e6f84d28 | ||
![]() |
5e22533fc0 | ||
![]() |
ecae074dd7 | ||
![]() |
55b0406960 | ||
![]() |
36483dd785 | ||
![]() |
ad154dce40 | ||
![]() |
3abf91af3a | ||
![]() |
579d217c6b | ||
![]() |
7322bee4dd | ||
![]() |
8a36ec88f4 | ||
![]() |
864f908257 | ||
![]() |
5d86d8b380 | ||
![]() |
15245707a5 | ||
![]() |
80e4451a3f | ||
![]() |
f051f4ea99 | ||
![]() |
7717b5aca6 | ||
![]() |
81d2bcdeb9 | ||
![]() |
9934de18ae | ||
![]() |
aac015e822 | ||
![]() |
1f584f011e | ||
![]() |
2106c4cfb9 | ||
![]() |
a053142601 | ||
![]() |
dd0dce7968 | ||
![]() |
bdfff6df2d | ||
![]() |
671c4e1eab | ||
![]() |
8aae2a935a | ||
![]() |
9e64f18439 | ||
![]() |
e8a6f2f098 | ||
![]() |
8faeb1fe98 | ||
![]() |
edc48e0604 | ||
![]() |
eab77f11b0 | ||
![]() |
edb79b0337 | ||
![]() |
41f33a106f | ||
![]() |
cf31401cc2 | ||
![]() |
8679c8e40c | ||
![]() |
e675d0e8ed | ||
![]() |
c73289aed9 | ||
![]() |
4420776977 | ||
![]() |
b77d6e7b59 | ||
![]() |
8f074e5724 | ||
![]() |
b1e46bcde4 | ||
![]() |
fc4b5f66ff | ||
![]() |
55978f2827 | ||
![]() |
010a8cc693 | ||
![]() |
d31eadc8cd | ||
![]() |
3190a523aa | ||
![]() |
8f82e451cd | ||
![]() |
5bbd71e594 | ||
![]() |
33257b8422 | ||
![]() |
b3a4cd5b76 | ||
![]() |
dcfa466dd4 | ||
![]() |
846e6d96a4 | ||
![]() |
d72b35a0cd | ||
![]() |
5e003627b2 | ||
![]() |
a4f71f37f6 | ||
![]() |
c37b2f86b1 | ||
![]() |
926aeef156 | ||
![]() |
ee86671d39 | ||
![]() |
dc371cf46d | ||
![]() |
c76e26508d | ||
![]() |
a5cd316fa3 | ||
![]() |
736cc8a17d | ||
![]() |
5278fce218 | ||
![]() |
8f04f22c65 | ||
![]() |
a01f638fc6 | ||
![]() |
22005dd48a | ||
![]() |
fff60b3863 | ||
![]() |
5cb5fe5b67 | ||
![]() |
24ea5eb9b5 | ||
![]() |
673c2a77e0 | ||
![]() |
ad3014e711 | ||
![]() |
c19ae81cbc | ||
![]() |
959d99f333 | ||
![]() |
7cfe6bf427 | ||
![]() |
765e2c1b6c | ||
![]() |
0fd63df123 | ||
![]() |
862fbd551a | ||
![]() |
6e79b76d15 | ||
![]() |
f85307d86c | ||
![]() |
b01f93119f | ||
![]() |
4130f3db2f | ||
![]() |
ffcd5167b5 | ||
![]() |
e94a7b2ec1 | ||
![]() |
6b3f2e9b7b | ||
![]() |
56545dacb0 | ||
![]() |
cbf061183e | ||
![]() |
5dcb5f4926 | ||
![]() |
5fbb99a79a | ||
![]() |
da65c52f2d | ||
![]() |
08a850cfc7 | ||
![]() |
12978092f7 | ||
![]() |
210a9ad2de | ||
![]() |
61328129fc | ||
![]() |
f4673f44ee | ||
![]() |
3bdd532dcd | ||
![]() |
e23d3c8ab4 | ||
![]() |
a7cb66c592 | ||
![]() |
8544d1ebec | ||
![]() |
240afd80c1 | ||
![]() |
ccb1da3a97 | ||
![]() |
de62991e5b | ||
![]() |
bad75222ed |
77
.claude/agents/quality-scale-rule-verifier.md
Normal file
77
.claude/agents/quality-scale-rule-verifier.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
name: quality-scale-rule-verifier
|
||||||
|
description: |
|
||||||
|
Use this agent when you need to verify that a Home Assistant integration follows a specific quality scale rule. This includes checking if the integration implements required patterns, configurations, or code structures defined by the quality scale system.
|
||||||
|
|
||||||
|
<example>
|
||||||
|
Context: The user wants to verify if an integration follows a specific quality scale rule.
|
||||||
|
user: "Check if the peblar integration follows the config-flow rule"
|
||||||
|
assistant: "I'll use the quality scale rule verifier to check if the peblar integration properly implements the config-flow rule."
|
||||||
|
<commentary>
|
||||||
|
Since the user is asking to verify a quality scale rule implementation, use the quality-scale-rule-verifier agent.
|
||||||
|
</commentary>
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example>
|
||||||
|
Context: The user is reviewing if an integration reaches a specific quality scale level.
|
||||||
|
user: "Verify that this integration reaches the bronze quality scale"
|
||||||
|
assistant: "Let me use the quality scale rule verifier to check the bronze quality scale implementation."
|
||||||
|
<commentary>
|
||||||
|
The user wants to verify the integration has reached a certain quality level, so use multiple quality-scale-rule-verifier agents to verify each bronze rule.
|
||||||
|
</commentary>
|
||||||
|
</example>
|
||||||
|
model: inherit
|
||||||
|
color: yellow
|
||||||
|
tools: Read, Bash, Grep, Glob, WebFetch
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an expert Home Assistant integration quality scale auditor specializing in verifying compliance with specific quality scale rules. You have deep knowledge of Home Assistant's architecture, best practices, and the quality scale system that ensures integration consistency and reliability.
|
||||||
|
|
||||||
|
You will verify if an integration follows a specific quality scale rule by:
|
||||||
|
|
||||||
|
1. **Fetching Rule Documentation**: Retrieve the official rule documentation from:
|
||||||
|
`https://raw.githubusercontent.com/home-assistant/developers.home-assistant/refs/heads/master/docs/core/integration-quality-scale/rules/{rule_name}.md`
|
||||||
|
where `{rule_name}` is the rule identifier (e.g., 'config-flow', 'entity-unique-id', 'parallel-updates')
|
||||||
|
|
||||||
|
2. **Understanding Rule Requirements**: Parse the rule documentation to identify:
|
||||||
|
- Core requirements and mandatory implementations
|
||||||
|
- Specific code patterns or configurations required
|
||||||
|
- Common violations and anti-patterns
|
||||||
|
- Exemption criteria (when a rule might not apply)
|
||||||
|
- The quality tier this rule belongs to (Bronze, Silver, Gold, Platinum)
|
||||||
|
|
||||||
|
3. **Analyzing Integration Code**: Examine the integration's codebase at `homeassistant/components/<integration domain>` focusing on:
|
||||||
|
- `manifest.json` for quality scale declaration and configuration
|
||||||
|
- `quality_scale.yaml` for rule status (done, todo, exempt)
|
||||||
|
- Relevant Python modules based on the rule requirements
|
||||||
|
- Configuration files and service definitions as needed
|
||||||
|
|
||||||
|
4. **Verification Process**:
|
||||||
|
- Check if the rule is marked as 'done', 'todo', or 'exempt' in quality_scale.yaml
|
||||||
|
- If marked 'exempt', verify the exemption reason is valid
|
||||||
|
- If marked 'done', verify the actual implementation matches requirements
|
||||||
|
- Identify specific files and code sections that demonstrate compliance or violations
|
||||||
|
- Consider the integration's declared quality tier when applying rules
|
||||||
|
- To fetch the integration docs, use WebFetch to fetch from `https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_integrations/<integration domain>.markdown`
|
||||||
|
- To fetch information about a PyPI package, use the URL `https://pypi.org/pypi/<package>/json`
|
||||||
|
|
||||||
|
5. **Reporting Findings**: Provide a comprehensive verification report that includes:
|
||||||
|
- **Rule Summary**: Brief description of what the rule requires
|
||||||
|
- **Compliance Status**: Clear pass/fail/exempt determination
|
||||||
|
- **Evidence**: Specific code examples showing compliance or violations
|
||||||
|
- **Issues Found**: Detailed list of any non-compliance issues with file locations
|
||||||
|
- **Recommendations**: Actionable steps to achieve compliance if needed
|
||||||
|
- **Exemption Analysis**: If applicable, whether the exemption is justified
|
||||||
|
|
||||||
|
When examining code, you will:
|
||||||
|
- Look for exact implementation patterns specified in the rule
|
||||||
|
- Verify all required components are present and properly configured
|
||||||
|
- Check for common mistakes and anti-patterns
|
||||||
|
- Consider edge cases and error handling requirements
|
||||||
|
- Validate that implementations follow Home Assistant conventions
|
||||||
|
|
||||||
|
You will be thorough but focused, examining only the aspects relevant to the specific rule being verified. You will provide clear, actionable feedback that helps developers understand both what needs to be fixed and why it matters for integration quality.
|
||||||
|
|
||||||
|
If you cannot access the rule documentation or find the integration code, clearly state what information is missing and what you would need to complete the verification.
|
||||||
|
|
||||||
|
Remember that quality scale rules are cumulative - Bronze rules apply to all integrations with a quality scale, Silver rules apply to Silver+ integrations, and so on. Always consider the integration's target quality level when determining which rules should be enforced.
|
@@ -58,6 +58,7 @@ base_platforms: &base_platforms
|
|||||||
# Extra components that trigger the full suite
|
# Extra components that trigger the full suite
|
||||||
components: &components
|
components: &components
|
||||||
- homeassistant/components/alexa/**
|
- homeassistant/components/alexa/**
|
||||||
|
- homeassistant/components/analytics/**
|
||||||
- homeassistant/components/application_credentials/**
|
- homeassistant/components/application_credentials/**
|
||||||
- homeassistant/components/assist_pipeline/**
|
- homeassistant/components/assist_pipeline/**
|
||||||
- homeassistant/components/auth/**
|
- homeassistant/components/auth/**
|
||||||
|
@@ -8,6 +8,8 @@
|
|||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
|
// Node feature required for Claude Code until fixed https://github.com/anthropics/devcontainer-features/issues/28
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {},
|
||||||
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
},
|
},
|
||||||
|
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -55,8 +55,12 @@
|
|||||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||||
We're here to help! This is simply a reminder of what we are going to look
|
We're here to help! This is simply a reminder of what we are going to look
|
||||||
for before merging your code.
|
for before merging your code.
|
||||||
|
|
||||||
|
AI tools are welcome, but contributors are responsible for *fully*
|
||||||
|
understanding the code before submitting a PR.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
- [ ] I understand the code I am submitting and can explain how it works.
|
||||||
- [ ] The code change is tested and works locally.
|
- [ ] The code change is tested and works locally.
|
||||||
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
||||||
- [ ] There is no commented out code in this PR.
|
- [ ] There is no commented out code in this PR.
|
||||||
@@ -64,6 +68,7 @@
|
|||||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||||
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
|
- [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`)
|
||||||
- [ ] Tests have been added to verify that the new code works.
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
|
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
|
|
||||||
|
50
.github/workflows/builder.yml
vendored
50
.github/workflows/builder.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -90,11 +90,11 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v11
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/frontend
|
repo: home-assistant/frontend
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of intents
|
- name: Download nightly wheels of intents
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v11
|
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: OHF-Voice/intents-package
|
repo: OHF-Voice/intents-package
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ jobs:
|
|||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -190,14 +190,15 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# home-assistant/builder doesn't support sha pinning
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2025.03.0
|
uses: home-assistant/builder@2025.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -242,7 +243,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -256,14 +257,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# home-assistant/builder doesn't support sha pinning
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2025.03.0
|
uses: home-assistant/builder@2025.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -279,7 +281,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -321,23 +323,23 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.9.2
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -454,15 +456,15 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -480,7 +482,7 @@ jobs:
|
|||||||
python -m build
|
python -m build
|
||||||
|
|
||||||
- name: Upload package to PyPI
|
- name: Upload package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||||
with:
|
with:
|
||||||
skip-existing: true
|
skip-existing: true
|
||||||
|
|
||||||
@@ -502,7 +504,7 @@ jobs:
|
|||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -531,7 +533,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
756
.github/workflows/ci.yaml
vendored
756
.github/workflows/ci.yaml
vendored
File diff suppressed because it is too large
Load Diff
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.29.11
|
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.29.11
|
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if integration label was added and extract details
|
- name: Check if integration label was added and extract details
|
||||||
id: extract
|
id: extract
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
// Debug: Log the event payload
|
// Debug: Log the event payload
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- name: Fetch similar issues
|
- name: Fetch similar issues
|
||||||
id: fetch_similar
|
id: fetch_similar
|
||||||
if: steps.extract.outputs.should_continue == 'true'
|
if: steps.extract.outputs.should_continue == 'true'
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
env:
|
env:
|
||||||
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
||||||
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@v2.0.1
|
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
@@ -280,7 +280,7 @@ jobs:
|
|||||||
- name: Post duplicate detection results
|
- name: Post duplicate detection results
|
||||||
id: post_results
|
id: post_results
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
env:
|
env:
|
||||||
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
||||||
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
||||||
|
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check issue language
|
- name: Check issue language
|
||||||
id: detect_language
|
id: detect_language
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
env:
|
env:
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@v2.0.1
|
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Process non-English issues
|
- name: Process non-English issues
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/github-script@v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
env:
|
env:
|
||||||
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
||||||
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v5.0.1
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
2
.github/workflows/restrict-task-creation.yml
vendored
2
.github/workflows/restrict-task-creation.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
if: github.event.issue.type.name == 'Task'
|
if: github.event.issue.type.name == 'Task'
|
||||||
steps:
|
steps:
|
||||||
- name: Check if user is authorized
|
- name: Check if user is authorized
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
const issueAuthor = context.payload.issue.user.login;
|
||||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@v9.1.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@v9.1.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@v9.1.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
36
.github/workflows/wheels.yml
vendored
36
.github/workflows/wheels.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
) > build_constraints.txt
|
) > build_constraints.txt
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
@@ -99,14 +99,14 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload build_constraints
|
- name: Upload build_constraints
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
path: ./build_constraints.txt
|
path: ./build_constraints.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@@ -135,20 +135,20 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -158,8 +158,9 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.07.0
|
uses: home-assistant/wheels@2025.09.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -184,25 +185,25 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
@@ -218,8 +219,9 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.07.0
|
uses: home-assistant/wheels@2025.09.1
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -140,5 +140,5 @@ tmp_cache
|
|||||||
pytest_buckets.txt
|
pytest_buckets.txt
|
||||||
|
|
||||||
# AI tooling
|
# AI tooling
|
||||||
.claude
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.1
|
rev: v0.13.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
|
@@ -142,6 +142,7 @@ homeassistant.components.cloud.*
|
|||||||
homeassistant.components.co2signal.*
|
homeassistant.components.co2signal.*
|
||||||
homeassistant.components.comelit.*
|
homeassistant.components.comelit.*
|
||||||
homeassistant.components.command_line.*
|
homeassistant.components.command_line.*
|
||||||
|
homeassistant.components.compit.*
|
||||||
homeassistant.components.config.*
|
homeassistant.components.config.*
|
||||||
homeassistant.components.configurator.*
|
homeassistant.components.configurator.*
|
||||||
homeassistant.components.cookidoo.*
|
homeassistant.components.cookidoo.*
|
||||||
@@ -169,6 +170,7 @@ homeassistant.components.dnsip.*
|
|||||||
homeassistant.components.doorbird.*
|
homeassistant.components.doorbird.*
|
||||||
homeassistant.components.dormakaba_dkey.*
|
homeassistant.components.dormakaba_dkey.*
|
||||||
homeassistant.components.downloader.*
|
homeassistant.components.downloader.*
|
||||||
|
homeassistant.components.droplet.*
|
||||||
homeassistant.components.dsmr.*
|
homeassistant.components.dsmr.*
|
||||||
homeassistant.components.duckdns.*
|
homeassistant.components.duckdns.*
|
||||||
homeassistant.components.dunehd.*
|
homeassistant.components.dunehd.*
|
||||||
@@ -201,6 +203,7 @@ homeassistant.components.feedreader.*
|
|||||||
homeassistant.components.file_upload.*
|
homeassistant.components.file_upload.*
|
||||||
homeassistant.components.filesize.*
|
homeassistant.components.filesize.*
|
||||||
homeassistant.components.filter.*
|
homeassistant.components.filter.*
|
||||||
|
homeassistant.components.firefly_iii.*
|
||||||
homeassistant.components.fitbit.*
|
homeassistant.components.fitbit.*
|
||||||
homeassistant.components.flexit_bacnet.*
|
homeassistant.components.flexit_bacnet.*
|
||||||
homeassistant.components.flux_led.*
|
homeassistant.components.flux_led.*
|
||||||
@@ -307,6 +310,7 @@ homeassistant.components.ld2410_ble.*
|
|||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
homeassistant.components.lektrico.*
|
||||||
homeassistant.components.letpot.*
|
homeassistant.components.letpot.*
|
||||||
|
homeassistant.components.libre_hardware_monitor.*
|
||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
@@ -322,6 +326,7 @@ homeassistant.components.london_underground.*
|
|||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.lovelace.*
|
homeassistant.components.lovelace.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
|
homeassistant.components.lunatone.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
@@ -382,6 +387,7 @@ homeassistant.components.openai_conversation.*
|
|||||||
homeassistant.components.openexchangerates.*
|
homeassistant.components.openexchangerates.*
|
||||||
homeassistant.components.opensky.*
|
homeassistant.components.opensky.*
|
||||||
homeassistant.components.openuv.*
|
homeassistant.components.openuv.*
|
||||||
|
homeassistant.components.opnsense.*
|
||||||
homeassistant.components.opower.*
|
homeassistant.components.opower.*
|
||||||
homeassistant.components.oralb.*
|
homeassistant.components.oralb.*
|
||||||
homeassistant.components.otbr.*
|
homeassistant.components.otbr.*
|
||||||
@@ -399,6 +405,7 @@ homeassistant.components.person.*
|
|||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
homeassistant.components.ping.*
|
homeassistant.components.ping.*
|
||||||
homeassistant.components.plugwise.*
|
homeassistant.components.plugwise.*
|
||||||
|
homeassistant.components.portainer.*
|
||||||
homeassistant.components.powerfox.*
|
homeassistant.components.powerfox.*
|
||||||
homeassistant.components.powerwall.*
|
homeassistant.components.powerwall.*
|
||||||
homeassistant.components.private_ble_device.*
|
homeassistant.components.private_ble_device.*
|
||||||
@@ -438,6 +445,7 @@ homeassistant.components.rituals_perfume_genie.*
|
|||||||
homeassistant.components.roborock.*
|
homeassistant.components.roborock.*
|
||||||
homeassistant.components.roku.*
|
homeassistant.components.roku.*
|
||||||
homeassistant.components.romy.*
|
homeassistant.components.romy.*
|
||||||
|
homeassistant.components.route_b_smart_meter.*
|
||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
homeassistant.components.rss_feed_template.*
|
homeassistant.components.rss_feed_template.*
|
||||||
homeassistant.components.russound_rio.*
|
homeassistant.components.russound_rio.*
|
||||||
@@ -458,6 +466,7 @@ homeassistant.components.sensorpush_cloud.*
|
|||||||
homeassistant.components.sensoterra.*
|
homeassistant.components.sensoterra.*
|
||||||
homeassistant.components.senz.*
|
homeassistant.components.senz.*
|
||||||
homeassistant.components.sfr_box.*
|
homeassistant.components.sfr_box.*
|
||||||
|
homeassistant.components.sftp_storage.*
|
||||||
homeassistant.components.shell_command.*
|
homeassistant.components.shell_command.*
|
||||||
homeassistant.components.shelly.*
|
homeassistant.components.shelly.*
|
||||||
homeassistant.components.shopping_list.*
|
homeassistant.components.shopping_list.*
|
||||||
@@ -546,6 +555,7 @@ homeassistant.components.vacuum.*
|
|||||||
homeassistant.components.vallox.*
|
homeassistant.components.vallox.*
|
||||||
homeassistant.components.valve.*
|
homeassistant.components.valve.*
|
||||||
homeassistant.components.velbus.*
|
homeassistant.components.velbus.*
|
||||||
|
homeassistant.components.vivotek.*
|
||||||
homeassistant.components.vlc_telnet.*
|
homeassistant.components.vlc_telnet.*
|
||||||
homeassistant.components.vodafone_station.*
|
homeassistant.components.vodafone_station.*
|
||||||
homeassistant.components.volvo.*
|
homeassistant.components.volvo.*
|
||||||
|
111
CODEOWNERS
generated
111
CODEOWNERS
generated
@@ -107,8 +107,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/ambient_station/ @bachya
|
/homeassistant/components/ambient_station/ @bachya
|
||||||
/tests/components/ambient_station/ @bachya
|
/tests/components/ambient_station/ @bachya
|
||||||
/homeassistant/components/amcrest/ @flacjacket
|
/homeassistant/components/amcrest/ @flacjacket
|
||||||
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
|
/homeassistant/components/analytics/ @home-assistant/core
|
||||||
/tests/components/analytics/ @home-assistant/core @ludeeus
|
/tests/components/analytics/ @home-assistant/core
|
||||||
/homeassistant/components/analytics_insights/ @joostlek
|
/homeassistant/components/analytics_insights/ @joostlek
|
||||||
/tests/components/analytics_insights/ @joostlek
|
/tests/components/analytics_insights/ @joostlek
|
||||||
/homeassistant/components/android_ip_webcam/ @engrbm87
|
/homeassistant/components/android_ip_webcam/ @engrbm87
|
||||||
@@ -154,10 +154,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/arve/ @ikalnyi
|
/tests/components/arve/ @ikalnyi
|
||||||
/homeassistant/components/aseko_pool_live/ @milanmeu
|
/homeassistant/components/aseko_pool_live/ @milanmeu
|
||||||
/tests/components/aseko_pool_live/ @milanmeu
|
/tests/components/aseko_pool_live/ @milanmeu
|
||||||
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
/homeassistant/components/assist_pipeline/ @synesthesiam @arturpragacz
|
||||||
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
/tests/components/assist_pipeline/ @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||||
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||||
/homeassistant/components/atag/ @MatsNL
|
/homeassistant/components/atag/ @MatsNL
|
||||||
@@ -292,14 +292,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/command_line/ @gjohansson-ST
|
/tests/components/command_line/ @gjohansson-ST
|
||||||
/homeassistant/components/compensation/ @Petro31
|
/homeassistant/components/compensation/ @Petro31
|
||||||
/tests/components/compensation/ @Petro31
|
/tests/components/compensation/ @Petro31
|
||||||
|
/homeassistant/components/compit/ @Przemko92
|
||||||
|
/tests/components/compit/ @Przemko92
|
||||||
/homeassistant/components/config/ @home-assistant/core
|
/homeassistant/components/config/ @home-assistant/core
|
||||||
/tests/components/config/ @home-assistant/core
|
/tests/components/config/ @home-assistant/core
|
||||||
/homeassistant/components/configurator/ @home-assistant/core
|
/homeassistant/components/configurator/ @home-assistant/core
|
||||||
/tests/components/configurator/ @home-assistant/core
|
/tests/components/configurator/ @home-assistant/core
|
||||||
/homeassistant/components/control4/ @lawtancool
|
/homeassistant/components/control4/ @lawtancool
|
||||||
/tests/components/control4/ @lawtancool
|
/tests/components/control4/ @lawtancool
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam
|
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/cookidoo/ @miaucl
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
/tests/components/cookidoo/ @miaucl
|
/tests/components/cookidoo/ @miaucl
|
||||||
/homeassistant/components/coolmaster/ @OnFreund
|
/homeassistant/components/coolmaster/ @OnFreund
|
||||||
@@ -314,6 +316,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/crownstone/ @Crownstone @RicArch97
|
/tests/components/crownstone/ @Crownstone @RicArch97
|
||||||
/homeassistant/components/cups/ @fabaff
|
/homeassistant/components/cups/ @fabaff
|
||||||
/tests/components/cups/ @fabaff
|
/tests/components/cups/ @fabaff
|
||||||
|
/homeassistant/components/cync/ @Kinachi249
|
||||||
|
/tests/components/cync/ @Kinachi249
|
||||||
/homeassistant/components/daikin/ @fredrike
|
/homeassistant/components/daikin/ @fredrike
|
||||||
/tests/components/daikin/ @fredrike
|
/tests/components/daikin/ @fredrike
|
||||||
/homeassistant/components/date/ @home-assistant/core
|
/homeassistant/components/date/ @home-assistant/core
|
||||||
@@ -377,6 +381,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dremel_3d_printer/ @tkdrob
|
/tests/components/dremel_3d_printer/ @tkdrob
|
||||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
|
/homeassistant/components/droplet/ @sarahseidman
|
||||||
|
/tests/components/droplet/ @sarahseidman
|
||||||
/homeassistant/components/dsmr/ @Robbie1221
|
/homeassistant/components/dsmr/ @Robbie1221
|
||||||
/tests/components/dsmr/ @Robbie1221
|
/tests/components/dsmr/ @Robbie1221
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
@@ -406,6 +412,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
/homeassistant/components/eheimdigital/ @autinerd
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
/tests/components/eheimdigital/ @autinerd
|
/tests/components/eheimdigital/ @autinerd
|
||||||
|
/homeassistant/components/ekeybionyx/ @richardpolzer
|
||||||
|
/tests/components/ekeybionyx/ @richardpolzer
|
||||||
/homeassistant/components/electrasmart/ @jafar-atili
|
/homeassistant/components/electrasmart/ @jafar-atili
|
||||||
/tests/components/electrasmart/ @jafar-atili
|
/tests/components/electrasmart/ @jafar-atili
|
||||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||||
@@ -440,8 +448,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/energyzero/ @klaasnicolaas
|
/tests/components/energyzero/ @klaasnicolaas
|
||||||
/homeassistant/components/enigma2/ @autinerd
|
/homeassistant/components/enigma2/ @autinerd
|
||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enocean/ @bdurrer
|
|
||||||
/tests/components/enocean/ @bdurrer
|
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||||
@@ -464,8 +470,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/eufylife_ble/ @bdr99
|
/tests/components/eufylife_ble/ @bdr99
|
||||||
/homeassistant/components/event/ @home-assistant/core
|
/homeassistant/components/event/ @home-assistant/core
|
||||||
/tests/components/event/ @home-assistant/core
|
/tests/components/event/ @home-assistant/core
|
||||||
/homeassistant/components/evil_genius_labs/ @balloob
|
|
||||||
/tests/components/evil_genius_labs/ @balloob
|
|
||||||
/homeassistant/components/evohome/ @zxdavb
|
/homeassistant/components/evohome/ @zxdavb
|
||||||
/tests/components/evohome/ @zxdavb
|
/tests/components/evohome/ @zxdavb
|
||||||
/homeassistant/components/ezviz/ @RenierM26
|
/homeassistant/components/ezviz/ @RenierM26
|
||||||
@@ -488,6 +492,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/filesize/ @gjohansson-ST
|
/tests/components/filesize/ @gjohansson-ST
|
||||||
/homeassistant/components/filter/ @dgomes
|
/homeassistant/components/filter/ @dgomes
|
||||||
/tests/components/filter/ @dgomes
|
/tests/components/filter/ @dgomes
|
||||||
|
/homeassistant/components/firefly_iii/ @erwindouna
|
||||||
|
/tests/components/firefly_iii/ @erwindouna
|
||||||
/homeassistant/components/fireservicerota/ @cyberjunky
|
/homeassistant/components/fireservicerota/ @cyberjunky
|
||||||
/tests/components/fireservicerota/ @cyberjunky
|
/tests/components/fireservicerota/ @cyberjunky
|
||||||
/homeassistant/components/firmata/ @DaAwesomeP
|
/homeassistant/components/firmata/ @DaAwesomeP
|
||||||
@@ -515,8 +521,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/forked_daapd/ @uvjustin
|
/homeassistant/components/forked_daapd/ @uvjustin
|
||||||
/tests/components/forked_daapd/ @uvjustin
|
/tests/components/forked_daapd/ @uvjustin
|
||||||
/homeassistant/components/fortios/ @kimfrellsen
|
/homeassistant/components/fortios/ @kimfrellsen
|
||||||
/homeassistant/components/foscam/ @krmarien
|
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
||||||
/tests/components/foscam/ @krmarien
|
/tests/components/foscam/ @Foscam-wangzhengyu
|
||||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
@@ -650,6 +656,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homeassistant/ @home-assistant/core
|
/tests/components/homeassistant/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||||
/tests/components/homeassistant_alerts/ @home-assistant/core
|
/tests/components/homeassistant_alerts/ @home-assistant/core
|
||||||
|
/homeassistant/components/homeassistant_connect_zbt2/ @home-assistant/core
|
||||||
|
/tests/components/homeassistant_connect_zbt2/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_green/ @home-assistant/core
|
/homeassistant/components/homeassistant_green/ @home-assistant/core
|
||||||
/tests/components/homeassistant_green/ @home-assistant/core
|
/tests/components/homeassistant_green/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
|
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
|
||||||
@@ -678,8 +686,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/http/ @home-assistant/core
|
/tests/components/http/ @home-assistant/core
|
||||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||||
/tests/components/huawei_lte/ @scop @fphammerle
|
/tests/components/huawei_lte/ @scop @fphammerle
|
||||||
/homeassistant/components/hue/ @balloob @marcelveldt
|
/homeassistant/components/hue/ @marcelveldt
|
||||||
/tests/components/hue/ @balloob @marcelveldt
|
/tests/components/hue/ @marcelveldt
|
||||||
/homeassistant/components/huisbaasje/ @dennisschroer
|
/homeassistant/components/huisbaasje/ @dennisschroer
|
||||||
/tests/components/huisbaasje/ @dennisschroer
|
/tests/components/huisbaasje/ @dennisschroer
|
||||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
@@ -751,8 +759,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/integration/ @dgomes
|
/tests/components/integration/ @dgomes
|
||||||
/homeassistant/components/intellifire/ @jeeftor
|
/homeassistant/components/intellifire/ @jeeftor
|
||||||
/tests/components/intellifire/ @jeeftor
|
/tests/components/intellifire/ @jeeftor
|
||||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam
|
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/intent/ @home-assistant/core @synesthesiam
|
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/intesishome/ @jnimmo
|
/homeassistant/components/intesishome/ @jnimmo
|
||||||
/homeassistant/components/iometer/ @MaestroOnICe
|
/homeassistant/components/iometer/ @MaestroOnICe
|
||||||
/tests/components/iometer/ @MaestroOnICe
|
/tests/components/iometer/ @MaestroOnICe
|
||||||
@@ -770,6 +778,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/iqvia/ @bachya
|
/homeassistant/components/iqvia/ @bachya
|
||||||
/tests/components/iqvia/ @bachya
|
/tests/components/iqvia/ @bachya
|
||||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||||
|
/homeassistant/components/irm_kmi/ @jdejaegh
|
||||||
|
/tests/components/irm_kmi/ @jdejaegh
|
||||||
/homeassistant/components/iron_os/ @tr4nt0r
|
/homeassistant/components/iron_os/ @tr4nt0r
|
||||||
/tests/components/iron_os/ @tr4nt0r
|
/tests/components/iron_os/ @tr4nt0r
|
||||||
/homeassistant/components/isal/ @bdraco
|
/homeassistant/components/isal/ @bdraco
|
||||||
@@ -860,6 +870,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
|
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||||
|
/tests/components/libre_hardware_monitor/ @Sab44
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/lifx/ @Djelibeybi
|
/homeassistant/components/lifx/ @Djelibeybi
|
||||||
@@ -898,6 +910,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/luci/ @mzdrale
|
/homeassistant/components/luci/ @mzdrale
|
||||||
/homeassistant/components/luftdaten/ @fabaff @frenck
|
/homeassistant/components/luftdaten/ @fabaff @frenck
|
||||||
/tests/components/luftdaten/ @fabaff @frenck
|
/tests/components/luftdaten/ @fabaff @frenck
|
||||||
|
/homeassistant/components/lunatone/ @MoonDevLT
|
||||||
|
/tests/components/lunatone/ @MoonDevLT
|
||||||
/homeassistant/components/lupusec/ @majuss @suaveolent
|
/homeassistant/components/lupusec/ @majuss @suaveolent
|
||||||
/tests/components/lupusec/ @majuss @suaveolent
|
/tests/components/lupusec/ @majuss @suaveolent
|
||||||
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
||||||
@@ -943,6 +957,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/met_eireann/ @DylanGore
|
/tests/components/met_eireann/ @DylanGore
|
||||||
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||||
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||||
|
/homeassistant/components/meteo_lt/ @xE1H
|
||||||
|
/tests/components/meteo_lt/ @xE1H
|
||||||
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
||||||
/homeassistant/components/meteoclimatic/ @adrianmo
|
/homeassistant/components/meteoclimatic/ @adrianmo
|
||||||
/tests/components/meteoclimatic/ @adrianmo
|
/tests/components/meteoclimatic/ @adrianmo
|
||||||
@@ -1013,7 +1029,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nanoleaf/ @milanmeu @joostlek
|
/tests/components/nanoleaf/ @milanmeu @joostlek
|
||||||
/homeassistant/components/nasweb/ @nasWebio
|
/homeassistant/components/nasweb/ @nasWebio
|
||||||
/tests/components/nasweb/ @nasWebio
|
/tests/components/nasweb/ @nasWebio
|
||||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
|
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||||
|
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
|
||||||
/homeassistant/components/ness_alarm/ @nickw444
|
/homeassistant/components/ness_alarm/ @nickw444
|
||||||
/tests/components/ness_alarm/ @nickw444
|
/tests/components/ness_alarm/ @nickw444
|
||||||
/homeassistant/components/nest/ @allenporter
|
/homeassistant/components/nest/ @allenporter
|
||||||
@@ -1048,6 +1065,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/nilu/ @hfurubotten
|
/homeassistant/components/nilu/ @hfurubotten
|
||||||
/homeassistant/components/nina/ @DeerMaximum
|
/homeassistant/components/nina/ @DeerMaximum
|
||||||
/tests/components/nina/ @DeerMaximum
|
/tests/components/nina/ @DeerMaximum
|
||||||
|
/homeassistant/components/nintendo_parental/ @pantherale0
|
||||||
|
/tests/components/nintendo_parental/ @pantherale0
|
||||||
/homeassistant/components/nissan_leaf/ @filcole
|
/homeassistant/components/nissan_leaf/ @filcole
|
||||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
@@ -1108,8 +1127,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
/homeassistant/components/open_router/ @joostlek
|
/homeassistant/components/open_router/ @joostlek
|
||||||
/tests/components/open_router/ @joostlek
|
/tests/components/open_router/ @joostlek
|
||||||
/homeassistant/components/openai_conversation/ @balloob
|
|
||||||
/tests/components/openai_conversation/ @balloob
|
|
||||||
/homeassistant/components/openerz/ @misialq
|
/homeassistant/components/openerz/ @misialq
|
||||||
/tests/components/openerz/ @misialq
|
/tests/components/openerz/ @misialq
|
||||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||||
@@ -1181,14 +1198,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/plex/ @jjlawren
|
/tests/components/plex/ @jjlawren
|
||||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||||
/tests/components/plugwise/ @CoMPaTech @bouwew
|
/tests/components/plugwise/ @CoMPaTech @bouwew
|
||||||
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
|
||||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
|
||||||
/homeassistant/components/point/ @fredrike
|
/homeassistant/components/point/ @fredrike
|
||||||
/tests/components/point/ @fredrike
|
/tests/components/point/ @fredrike
|
||||||
/homeassistant/components/pooldose/ @lmaertin
|
/homeassistant/components/pooldose/ @lmaertin
|
||||||
/tests/components/pooldose/ @lmaertin
|
/tests/components/pooldose/ @lmaertin
|
||||||
/homeassistant/components/poolsense/ @haemishkyd
|
/homeassistant/components/poolsense/ @haemishkyd
|
||||||
/tests/components/poolsense/ @haemishkyd
|
/tests/components/poolsense/ @haemishkyd
|
||||||
|
/homeassistant/components/portainer/ @erwindouna
|
||||||
|
/tests/components/portainer/ @erwindouna
|
||||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||||
/tests/components/powerfox/ @klaasnicolaas
|
/tests/components/powerfox/ @klaasnicolaas
|
||||||
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||||
@@ -1208,8 +1225,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/proximity/ @mib1185
|
/homeassistant/components/proximity/ @mib1185
|
||||||
/tests/components/proximity/ @mib1185
|
/tests/components/proximity/ @mib1185
|
||||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||||
/homeassistant/components/prusalink/ @balloob
|
|
||||||
/tests/components/prusalink/ @balloob
|
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
/homeassistant/components/pterodactyl/ @elmurato
|
/homeassistant/components/pterodactyl/ @elmurato
|
||||||
@@ -1303,8 +1318,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rflink/ @javicalle
|
/tests/components/rflink/ @javicalle
|
||||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
/tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
/homeassistant/components/rhasspy/ @balloob @synesthesiam
|
/homeassistant/components/rhasspy/ @synesthesiam
|
||||||
/tests/components/rhasspy/ @balloob @synesthesiam
|
/tests/components/rhasspy/ @synesthesiam
|
||||||
/homeassistant/components/ridwell/ @bachya
|
/homeassistant/components/ridwell/ @bachya
|
||||||
/tests/components/ridwell/ @bachya
|
/tests/components/ridwell/ @bachya
|
||||||
/homeassistant/components/ring/ @sdb9696
|
/homeassistant/components/ring/ @sdb9696
|
||||||
@@ -1325,6 +1340,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||||
/homeassistant/components/roon/ @pavoni
|
/homeassistant/components/roon/ @pavoni
|
||||||
/tests/components/roon/ @pavoni
|
/tests/components/roon/ @pavoni
|
||||||
|
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
|
||||||
|
/tests/components/route_b_smart_meter/ @SeraphicRav
|
||||||
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
||||||
/tests/components/rpi_power/ @shenxn @swetoast
|
/tests/components/rpi_power/ @shenxn @swetoast
|
||||||
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
||||||
@@ -1347,6 +1364,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/samsungtv/ @chemelli74 @epenet
|
/tests/components/samsungtv/ @chemelli74 @epenet
|
||||||
/homeassistant/components/sanix/ @tomaszsluszniak
|
/homeassistant/components/sanix/ @tomaszsluszniak
|
||||||
/tests/components/sanix/ @tomaszsluszniak
|
/tests/components/sanix/ @tomaszsluszniak
|
||||||
|
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||||
|
/tests/components/satel_integra/ @Tommatheussen
|
||||||
/homeassistant/components/scene/ @home-assistant/core
|
/homeassistant/components/scene/ @home-assistant/core
|
||||||
/tests/components/scene/ @home-assistant/core
|
/tests/components/scene/ @home-assistant/core
|
||||||
/homeassistant/components/schedule/ @home-assistant/core
|
/homeassistant/components/schedule/ @home-assistant/core
|
||||||
@@ -1392,12 +1411,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/seventeentrack/ @shaiu
|
/tests/components/seventeentrack/ @shaiu
|
||||||
/homeassistant/components/sfr_box/ @epenet
|
/homeassistant/components/sfr_box/ @epenet
|
||||||
/tests/components/sfr_box/ @epenet
|
/tests/components/sfr_box/ @epenet
|
||||||
|
/homeassistant/components/sftp_storage/ @maretodoric
|
||||||
|
/tests/components/sftp_storage/ @maretodoric
|
||||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/tests/components/sharkiq/ @JeffResc @funkybunch
|
/tests/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/homeassistant/components/shell_command/ @home-assistant/core
|
/homeassistant/components/shell_command/ @home-assistant/core
|
||||||
/tests/components/shell_command/ @home-assistant/core
|
/tests/components/shell_command/ @home-assistant/core
|
||||||
/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
|
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
||||||
/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
|
/tests/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
||||||
/homeassistant/components/shodan/ @fabaff
|
/homeassistant/components/shodan/ @fabaff
|
||||||
/homeassistant/components/sia/ @eavanvalkenburg
|
/homeassistant/components/sia/ @eavanvalkenburg
|
||||||
/tests/components/sia/ @eavanvalkenburg
|
/tests/components/sia/ @eavanvalkenburg
|
||||||
@@ -1526,8 +1547,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/switchbee/ @jafar-atili
|
/tests/components/switchbee/ @jafar-atili
|
||||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
|
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
|
||||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
|
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
|
||||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
|
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
|
||||||
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
|
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
|
||||||
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
|
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
|
||||||
/tests/components/switcher_kis/ @thecode @YogevBokobza
|
/tests/components/switcher_kis/ @thecode @YogevBokobza
|
||||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||||
@@ -1544,8 +1565,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/systemmonitor/ @gjohansson-ST
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
/homeassistant/components/tado/ @erwindouna
|
/homeassistant/components/tado/ @erwindouna
|
||||||
/tests/components/tado/ @erwindouna
|
/tests/components/tado/ @erwindouna
|
||||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
/homeassistant/components/tag/ @home-assistant/core
|
||||||
/tests/components/tag/ @balloob @dmulcahey
|
/tests/components/tag/ @home-assistant/core
|
||||||
/homeassistant/components/tailscale/ @frenck
|
/homeassistant/components/tailscale/ @frenck
|
||||||
/tests/components/tailscale/ @frenck
|
/tests/components/tailscale/ @frenck
|
||||||
/homeassistant/components/tailwind/ @frenck
|
/homeassistant/components/tailwind/ @frenck
|
||||||
@@ -1672,6 +1693,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/uptime_kuma/ @tr4nt0r
|
/tests/components/uptime_kuma/ @tr4nt0r
|
||||||
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
|
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
|
||||||
/tests/components/uptimerobot/ @ludeeus @chemelli74
|
/tests/components/uptimerobot/ @ludeeus @chemelli74
|
||||||
|
/homeassistant/components/usage_prediction/ @home-assistant/core
|
||||||
|
/tests/components/usage_prediction/ @home-assistant/core
|
||||||
/homeassistant/components/usb/ @bdraco
|
/homeassistant/components/usb/ @bdraco
|
||||||
/tests/components/usb/ @bdraco
|
/tests/components/usb/ @bdraco
|
||||||
/homeassistant/components/usgs_earthquakes_feed/ @exxamalte
|
/homeassistant/components/usgs_earthquakes_feed/ @exxamalte
|
||||||
@@ -1690,17 +1713,19 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vegehub/ @ghowevege
|
/tests/components/vegehub/ @ghowevege
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
||||||
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
||||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||||
/tests/components/venstar/ @garbled1 @jhollowe
|
/tests/components/venstar/ @garbled1 @jhollowe
|
||||||
/homeassistant/components/versasense/ @imstevenxyz
|
/homeassistant/components/versasense/ @imstevenxyz
|
||||||
/homeassistant/components/version/ @ludeeus
|
/homeassistant/components/version/ @ludeeus
|
||||||
/tests/components/version/ @ludeeus
|
/tests/components/version/ @ludeeus
|
||||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
|
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
||||||
|
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
/tests/components/vilfo/ @ManneW
|
/tests/components/vilfo/ @ManneW
|
||||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||||
@@ -1710,14 +1735,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
/homeassistant/components/voip/ @synesthesiam @jaminh
|
||||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
/tests/components/voip/ @synesthesiam @jaminh
|
||||||
/homeassistant/components/volumio/ @OnFreund
|
/homeassistant/components/volumio/ @OnFreund
|
||||||
/tests/components/volumio/ @OnFreund
|
/tests/components/volumio/ @OnFreund
|
||||||
/homeassistant/components/volvo/ @thomasddn
|
/homeassistant/components/volvo/ @thomasddn
|
||||||
/tests/components/volvo/ @thomasddn
|
/tests/components/volvo/ @thomasddn
|
||||||
/homeassistant/components/volvooncall/ @molobrakos
|
/homeassistant/components/volvooncall/ @molobrakos @svrooij
|
||||||
/tests/components/volvooncall/ @molobrakos
|
/tests/components/volvooncall/ @molobrakos @svrooij
|
||||||
/homeassistant/components/wake_on_lan/ @ntilley905
|
/homeassistant/components/wake_on_lan/ @ntilley905
|
||||||
/tests/components/wake_on_lan/ @ntilley905
|
/tests/components/wake_on_lan/ @ntilley905
|
||||||
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
|
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
|
||||||
@@ -1782,8 +1807,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/worldclock/ @fabaff
|
/tests/components/worldclock/ @fabaff
|
||||||
/homeassistant/components/ws66i/ @ssaenger
|
/homeassistant/components/ws66i/ @ssaenger
|
||||||
/tests/components/ws66i/ @ssaenger
|
/tests/components/ws66i/ @ssaenger
|
||||||
/homeassistant/components/wyoming/ @balloob @synesthesiam
|
/homeassistant/components/wyoming/ @synesthesiam
|
||||||
/tests/components/wyoming/ @balloob @synesthesiam
|
/tests/components/wyoming/ @synesthesiam
|
||||||
/homeassistant/components/xbox/ @hunterjm
|
/homeassistant/components/xbox/ @hunterjm
|
||||||
/tests/components/xbox/ @hunterjm
|
/tests/components/xbox/ @hunterjm
|
||||||
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||||
|
@@ -3,8 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
|||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
apt-get update \
|
||||||
&& apt-get update \
|
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
||||||
bluez \
|
bluez \
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.0
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.0
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.0
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.0
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@@ -187,36 +187,42 @@ def main() -> int:
|
|||||||
|
|
||||||
from . import config, runner # noqa: PLC0415
|
from . import config, runner # noqa: PLC0415
|
||||||
|
|
||||||
safe_mode = config.safe_mode_enabled(config_dir)
|
# Ensure only one instance runs per config directory
|
||||||
|
with runner.ensure_single_execution(config_dir) as single_execution_lock:
|
||||||
|
# Check if another instance is already running
|
||||||
|
if single_execution_lock.exit_code is not None:
|
||||||
|
return single_execution_lock.exit_code
|
||||||
|
|
||||||
runtime_conf = runner.RuntimeConfig(
|
safe_mode = config.safe_mode_enabled(config_dir)
|
||||||
config_dir=config_dir,
|
|
||||||
verbose=args.verbose,
|
|
||||||
log_rotate_days=args.log_rotate_days,
|
|
||||||
log_file=args.log_file,
|
|
||||||
log_no_color=args.log_no_color,
|
|
||||||
skip_pip=args.skip_pip,
|
|
||||||
skip_pip_packages=args.skip_pip_packages,
|
|
||||||
recovery_mode=args.recovery_mode,
|
|
||||||
debug=args.debug,
|
|
||||||
open_ui=args.open_ui,
|
|
||||||
safe_mode=safe_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
runtime_conf = runner.RuntimeConfig(
|
||||||
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
|
config_dir=config_dir,
|
||||||
faulthandler.enable(fault_file)
|
verbose=args.verbose,
|
||||||
exit_code = runner.run(runtime_conf)
|
log_rotate_days=args.log_rotate_days,
|
||||||
faulthandler.disable()
|
log_file=args.log_file,
|
||||||
|
log_no_color=args.log_no_color,
|
||||||
|
skip_pip=args.skip_pip,
|
||||||
|
skip_pip_packages=args.skip_pip_packages,
|
||||||
|
recovery_mode=args.recovery_mode,
|
||||||
|
debug=args.debug,
|
||||||
|
open_ui=args.open_ui,
|
||||||
|
safe_mode=safe_mode,
|
||||||
|
)
|
||||||
|
|
||||||
# It's possible for the fault file to disappear, so suppress obvious errors
|
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
||||||
with suppress(FileNotFoundError):
|
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
|
||||||
if os.path.getsize(fault_file_name) == 0:
|
faulthandler.enable(fault_file)
|
||||||
os.remove(fault_file_name)
|
exit_code = runner.run(runtime_conf)
|
||||||
|
faulthandler.disable()
|
||||||
|
|
||||||
check_threads()
|
# It's possible for the fault file to disappear, so suppress obvious errors
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
if os.path.getsize(fault_file_name) == 0:
|
||||||
|
os.remove(fault_file_name)
|
||||||
|
|
||||||
return exit_code
|
check_threads()
|
||||||
|
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@@ -27,7 +27,7 @@ from . import (
|
|||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.8.0"]
|
REQUIREMENTS = ["pyotp==2.9.0"]
|
||||||
|
|
||||||
CONF_MESSAGE = "message"
|
CONF_MESSAGE = "message"
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ from . import (
|
|||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.8.0", "PyQRCode==1.2.1"]
|
REQUIREMENTS = ["pyotp==2.9.0", "PyQRCode==1.2.1"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
@@ -616,34 +616,34 @@ async def async_enable_logging(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log errors to a file if we have write access to file or config dir
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||||
|
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
|
if "SUPERVISOR" in os.environ:
|
||||||
|
_LOGGER.info("Running in Supervisor, not logging to file")
|
||||||
|
# Rename the default log file if it exists, since previous versions created
|
||||||
|
# it even on Supervisor
|
||||||
|
if os.path.isfile(default_log_path):
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
os.rename(default_log_path, f"{default_log_path}.old")
|
||||||
|
err_log_path = None
|
||||||
|
else:
|
||||||
|
err_log_path = default_log_path
|
||||||
else:
|
else:
|
||||||
err_log_path = os.path.abspath(log_file)
|
err_log_path = os.path.abspath(log_file)
|
||||||
|
|
||||||
err_path_exists = os.path.isfile(err_log_path)
|
if err_log_path:
|
||||||
err_dir = os.path.dirname(err_log_path)
|
|
||||||
|
|
||||||
# Check if we can write to the error log if it exists or that
|
|
||||||
# we can create files in the containing directory if not.
|
|
||||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
|
||||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
|
||||||
):
|
|
||||||
err_handler = await hass.async_add_executor_job(
|
err_handler = await hass.async_add_executor_job(
|
||||||
_create_log_file, err_log_path, log_rotate_days
|
_create_log_file, err_log_path, log_rotate_days
|
||||||
)
|
)
|
||||||
|
|
||||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.addHandler(err_handler)
|
logger.addHandler(err_handler)
|
||||||
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
|
||||||
|
|
||||||
# Save the log file location for access by other components.
|
# Save the log file location for access by other components.
|
||||||
hass.data[DATA_LOGGING] = err_log_path
|
hass.data[DATA_LOGGING] = err_log_path
|
||||||
else:
|
|
||||||
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
|
||||||
|
|
||||||
async_activate_log_queue_handler(hass)
|
async_activate_log_queue_handler(hass)
|
||||||
|
|
||||||
|
5
homeassistant/brands/eltako.json
Normal file
5
homeassistant/brands/eltako.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "eltako",
|
||||||
|
"name": "Eltako",
|
||||||
|
"iot_standards": ["matter"]
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "fritzbox",
|
"domain": "fritzbox",
|
||||||
"name": "FRITZ!Box",
|
"name": "FRITZ!",
|
||||||
"integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"]
|
"integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"]
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
"google_assistant_sdk",
|
"google_assistant_sdk",
|
||||||
"google_cloud",
|
"google_cloud",
|
||||||
"google_drive",
|
"google_drive",
|
||||||
"google_gemini",
|
|
||||||
"google_generative_ai_conversation",
|
"google_generative_ai_conversation",
|
||||||
"google_mail",
|
"google_mail",
|
||||||
"google_maps",
|
"google_maps",
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "ibm",
|
|
||||||
"name": "IBM",
|
|
||||||
"integrations": ["watson_iot", "watson_tts"]
|
|
||||||
}
|
|
5
homeassistant/brands/konnected.json
Normal file
5
homeassistant/brands/konnected.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "konnected",
|
||||||
|
"name": "Konnected",
|
||||||
|
"integrations": ["konnected", "konnected_esphome"]
|
||||||
|
}
|
5
homeassistant/brands/level.json
Normal file
5
homeassistant/brands/level.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "level",
|
||||||
|
"name": "Level",
|
||||||
|
"iot_standards": ["matter"]
|
||||||
|
}
|
@@ -8,14 +8,17 @@ import logging
|
|||||||
from aioacaia.acaiascale import AcaiaScale
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||||
|
|
||||||
|
from homeassistant.components.bluetooth import async_get_scanner
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS
|
from homeassistant.const import CONF_ADDRESS
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import CONF_IS_NEW_STYLE_SCALE
|
from .const import CONF_IS_NEW_STYLE_SCALE
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
UPDATE_DEBOUNCE_TIME = 0.2
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -37,11 +40,20 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
|||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
debouncer = Debouncer(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
cooldown=UPDATE_DEBOUNCE_TIME,
|
||||||
|
immediate=True,
|
||||||
|
function=self.async_update_listeners,
|
||||||
|
)
|
||||||
|
|
||||||
self._scale = AcaiaScale(
|
self._scale = AcaiaScale(
|
||||||
address_or_ble_device=entry.data[CONF_ADDRESS],
|
address_or_ble_device=entry.data[CONF_ADDRESS],
|
||||||
name=entry.title,
|
name=entry.title,
|
||||||
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
||||||
notify_callback=self.async_update_listeners,
|
notify_callback=debouncer.async_schedule_call,
|
||||||
|
scanner=async_get_scanner(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -26,5 +26,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioacaia"],
|
"loggers": ["aioacaia"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioacaia==0.1.14"]
|
"requirements": ["aioacaia==0.1.17"]
|
||||||
}
|
}
|
||||||
|
@@ -2,21 +2,23 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from accuweather import AccuWeather
|
from accuweather import AccuWeather
|
||||||
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
from homeassistant.const import CONF_API_KEY, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
|
from .const import DOMAIN
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
AccuWeatherConfigEntry,
|
AccuWeatherConfigEntry,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
AccuWeatherData,
|
AccuWeatherData,
|
||||||
|
AccuWeatherHourlyForecastDataUpdateCoordinator,
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +30,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
|
||||||
"""Set up AccuWeather as config entry."""
|
"""Set up AccuWeather as config entry."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
name: str = entry.data[CONF_NAME]
|
|
||||||
|
|
||||||
location_key = entry.unique_id
|
location_key = entry.unique_id
|
||||||
|
|
||||||
@@ -41,26 +42,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
|
|||||||
hass,
|
hass,
|
||||||
entry,
|
entry,
|
||||||
accuweather,
|
accuweather,
|
||||||
name,
|
|
||||||
"observation",
|
|
||||||
UPDATE_INTERVAL_OBSERVATION,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
|
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
entry,
|
entry,
|
||||||
accuweather,
|
accuweather,
|
||||||
name,
|
)
|
||||||
"daily forecast",
|
coordinator_hourly_forecast = AccuWeatherHourlyForecastDataUpdateCoordinator(
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST,
|
hass,
|
||||||
|
entry,
|
||||||
|
accuweather,
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator_observation.async_config_entry_first_refresh()
|
await asyncio.gather(
|
||||||
await coordinator_daily_forecast.async_config_entry_first_refresh()
|
coordinator_observation.async_config_entry_first_refresh(),
|
||||||
|
coordinator_daily_forecast.async_config_entry_first_refresh(),
|
||||||
|
coordinator_hourly_forecast.async_config_entry_first_refresh(),
|
||||||
|
)
|
||||||
|
|
||||||
entry.runtime_data = AccuWeatherData(
|
entry.runtime_data = AccuWeatherData(
|
||||||
coordinator_observation=coordinator_observation,
|
coordinator_observation=coordinator_observation,
|
||||||
coordinator_daily_forecast=coordinator_daily_forecast,
|
coordinator_daily_forecast=coordinator_daily_forecast,
|
||||||
|
coordinator_hourly_forecast=coordinator_hourly_forecast,
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||||
@@ -22,6 +23,8 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Config flow for AccuWeather."""
|
"""Config flow for AccuWeather."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
_latitude: float | None = None
|
||||||
|
_longitude: float | None = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@@ -50,6 +53,7 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.async_set_unique_id(
|
await self.async_set_unique_id(
|
||||||
accuweather.location_key, raise_on_progress=False
|
accuweather.location_key, raise_on_progress=False
|
||||||
)
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_NAME], data=user_input
|
title=user_input[CONF_NAME], data=user_input
|
||||||
@@ -73,3 +77,46 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle configuration by re-auth."""
|
||||||
|
self._latitude = entry_data[CONF_LATITUDE]
|
||||||
|
self._longitude = entry_data[CONF_LONGITUDE]
|
||||||
|
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Dialog that informs the user that reauth is required."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
websession = async_get_clientsession(self.hass)
|
||||||
|
try:
|
||||||
|
async with timeout(10):
|
||||||
|
accuweather = AccuWeather(
|
||||||
|
user_input[CONF_API_KEY],
|
||||||
|
websession,
|
||||||
|
latitude=self._latitude,
|
||||||
|
longitude=self._longitude,
|
||||||
|
)
|
||||||
|
await accuweather.async_get_location()
|
||||||
|
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidApiKeyError:
|
||||||
|
errors["base"] = "invalid_api_key"
|
||||||
|
except RequestsExceededError:
|
||||||
|
errors["base"] = "requests_exceeded"
|
||||||
|
else:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(), data_updates=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
@@ -69,5 +69,6 @@ POLLEN_CATEGORY_MAP = {
|
|||||||
4: "very_high",
|
4: "very_high",
|
||||||
5: "extreme",
|
5: "extreme",
|
||||||
}
|
}
|
||||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||||
|
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +13,9 @@ from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExcee
|
|||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
@@ -20,9 +23,15 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
UpdateFailed,
|
UpdateFailed,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
|
UPDATE_INTERVAL_DAILY_FORECAST,
|
||||||
|
UPDATE_INTERVAL_HOURLY_FORECAST,
|
||||||
|
UPDATE_INTERVAL_OBSERVATION,
|
||||||
|
)
|
||||||
|
|
||||||
EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError)
|
EXCEPTIONS = (ApiError, ClientConnectorError, RequestsExceededError)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -33,6 +42,7 @@ class AccuWeatherData:
|
|||||||
|
|
||||||
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
|
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
|
||||||
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
||||||
|
coordinator_hourly_forecast: AccuWeatherHourlyForecastDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
|
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
|
||||||
@@ -43,18 +53,18 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
|||||||
):
|
):
|
||||||
"""Class to manage fetching AccuWeather data API."""
|
"""Class to manage fetching AccuWeather data API."""
|
||||||
|
|
||||||
|
config_entry: AccuWeatherConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: AccuWeatherConfigEntry,
|
config_entry: AccuWeatherConfigEntry,
|
||||||
accuweather: AccuWeather,
|
accuweather: AccuWeather,
|
||||||
name: str,
|
|
||||||
coordinator_type: str,
|
|
||||||
update_interval: timedelta,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.accuweather = accuweather
|
self.accuweather = accuweather
|
||||||
self.location_key = accuweather.location_key
|
self.location_key = accuweather.location_key
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert self.location_key is not None
|
assert self.location_key is not None
|
||||||
@@ -65,8 +75,8 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
|||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
name=f"{name} ({coordinator_type})",
|
name=f"{name} (observation)",
|
||||||
update_interval=update_interval,
|
update_interval=UPDATE_INTERVAL_OBSERVATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
@@ -80,29 +90,39 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
|||||||
translation_key="current_conditions_update_error",
|
translation_key="current_conditions_update_error",
|
||||||
translation_placeholders={"error": repr(error)},
|
translation_placeholders={"error": repr(error)},
|
||||||
) from error
|
) from error
|
||||||
|
except InvalidApiKeyError as err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
translation_placeholders={"entry": self.config_entry.title},
|
||||||
|
) from err
|
||||||
|
|
||||||
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class AccuWeatherDailyForecastDataUpdateCoordinator(
|
class AccuWeatherForecastDataUpdateCoordinator(
|
||||||
TimestampDataUpdateCoordinator[list[dict[str, Any]]]
|
TimestampDataUpdateCoordinator[list[dict[str, Any]]]
|
||||||
):
|
):
|
||||||
"""Class to manage fetching AccuWeather data API."""
|
"""Base class for AccuWeather forecast."""
|
||||||
|
|
||||||
|
config_entry: AccuWeatherConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: AccuWeatherConfigEntry,
|
config_entry: AccuWeatherConfigEntry,
|
||||||
accuweather: AccuWeather,
|
accuweather: AccuWeather,
|
||||||
name: str,
|
|
||||||
coordinator_type: str,
|
coordinator_type: str,
|
||||||
update_interval: timedelta,
|
update_interval: timedelta,
|
||||||
|
fetch_method: Callable[..., Awaitable[list[dict[str, Any]]]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.accuweather = accuweather
|
self.accuweather = accuweather
|
||||||
self.location_key = accuweather.location_key
|
self.location_key = accuweather.location_key
|
||||||
|
self._fetch_method = fetch_method
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert self.location_key is not None
|
assert self.location_key is not None
|
||||||
@@ -118,24 +138,71 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||||
"""Update data via library."""
|
"""Update forecast data via library."""
|
||||||
try:
|
try:
|
||||||
async with timeout(10):
|
async with timeout(10):
|
||||||
result = await self.accuweather.async_get_daily_forecast(
|
result = await self._fetch_method(language=self.hass.config.language)
|
||||||
language=self.hass.config.language
|
|
||||||
)
|
|
||||||
except EXCEPTIONS as error:
|
except EXCEPTIONS as error:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="forecast_update_error",
|
translation_key="forecast_update_error",
|
||||||
translation_placeholders={"error": repr(error)},
|
translation_placeholders={"error": repr(error)},
|
||||||
) from error
|
) from error
|
||||||
|
except InvalidApiKeyError as err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
translation_placeholders={"entry": self.config_entry.title},
|
||||||
|
) from err
|
||||||
|
|
||||||
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||||
|
AccuWeatherForecastDataUpdateCoordinator
|
||||||
|
):
|
||||||
|
"""Coordinator for daily forecast."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AccuWeatherConfigEntry,
|
||||||
|
accuweather: AccuWeather,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
accuweather,
|
||||||
|
"daily forecast",
|
||||||
|
UPDATE_INTERVAL_DAILY_FORECAST,
|
||||||
|
fetch_method=accuweather.async_get_daily_forecast,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccuWeatherHourlyForecastDataUpdateCoordinator(
|
||||||
|
AccuWeatherForecastDataUpdateCoordinator
|
||||||
|
):
|
||||||
|
"""Coordinator for hourly forecast."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AccuWeatherConfigEntry,
|
||||||
|
accuweather: AccuWeather,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
accuweather,
|
||||||
|
"hourly forecast",
|
||||||
|
UPDATE_INTERVAL_HOURLY_FORECAST,
|
||||||
|
fetch_method=accuweather.async_get_hourly_forecast,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_device_info(location_key: str, name: str) -> DeviceInfo:
|
def _get_device_info(location_key: str, name: str) -> DeviceInfo:
|
||||||
"""Get device info."""
|
"""Get device info."""
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
|
@@ -7,6 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["accuweather"],
|
"loggers": ["accuweather"],
|
||||||
"requirements": ["accuweather==4.2.0"],
|
"requirements": ["accuweather==4.2.2"]
|
||||||
"single_config_entry": true
|
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,17 @@
|
|||||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||||
"longitude": "[%key:common::config_flow::data::longitude%]"
|
"longitude": "[%key:common::config_flow::data::longitude%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"api_key": "API key generated in the AccuWeather APIs portal."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17,6 +28,10 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||||
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
|
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -236,6 +251,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"auth_error": {
|
||||||
|
"message": "Authentication failed for {entry}, please update your API key"
|
||||||
|
},
|
||||||
"current_conditions_update_error": {
|
"current_conditions_update_error": {
|
||||||
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
|
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
|
||||||
},
|
},
|
||||||
|
@@ -45,6 +45,7 @@ from .coordinator import (
|
|||||||
AccuWeatherConfigEntry,
|
AccuWeatherConfigEntry,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
AccuWeatherData,
|
AccuWeatherData,
|
||||||
|
AccuWeatherHourlyForecastDataUpdateCoordinator,
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ class AccuWeatherEntity(
|
|||||||
CoordinatorWeatherEntity[
|
CoordinatorWeatherEntity[
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
|
AccuWeatherHourlyForecastDataUpdateCoordinator,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
"""Define an AccuWeather entity."""
|
"""Define an AccuWeather entity."""
|
||||||
@@ -76,6 +78,7 @@ class AccuWeatherEntity(
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
observation_coordinator=accuweather_data.coordinator_observation,
|
observation_coordinator=accuweather_data.coordinator_observation,
|
||||||
daily_coordinator=accuweather_data.coordinator_daily_forecast,
|
daily_coordinator=accuweather_data.coordinator_daily_forecast,
|
||||||
|
hourly_coordinator=accuweather_data.coordinator_hourly_forecast,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||||
@@ -86,10 +89,13 @@ class AccuWeatherEntity(
|
|||||||
self._attr_unique_id = accuweather_data.coordinator_observation.location_key
|
self._attr_unique_id = accuweather_data.coordinator_observation.location_key
|
||||||
self._attr_attribution = ATTRIBUTION
|
self._attr_attribution = ATTRIBUTION
|
||||||
self._attr_device_info = accuweather_data.coordinator_observation.device_info
|
self._attr_device_info = accuweather_data.coordinator_observation.device_info
|
||||||
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
|
self._attr_supported_features = (
|
||||||
|
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||||
|
)
|
||||||
|
|
||||||
self.observation_coordinator = accuweather_data.coordinator_observation
|
self.observation_coordinator = accuweather_data.coordinator_observation
|
||||||
self.daily_coordinator = accuweather_data.coordinator_daily_forecast
|
self.daily_coordinator = accuweather_data.coordinator_daily_forecast
|
||||||
|
self.hourly_coordinator = accuweather_data.coordinator_hourly_forecast
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def condition(self) -> str | None:
|
def condition(self) -> str | None:
|
||||||
@@ -207,3 +213,32 @@ class AccuWeatherEntity(
|
|||||||
}
|
}
|
||||||
for item in self.daily_coordinator.data
|
for item in self.daily_coordinator.data
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||||
|
"""Return the hourly forecast in native units."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_TIME: utc_from_timestamp(
|
||||||
|
item["EpochDateTime"]
|
||||||
|
).isoformat(),
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCover"],
|
||||||
|
ATTR_FORECAST_HUMIDITY: item["RelativeHumidity"],
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP: item["Temperature"][ATTR_VALUE],
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperature"][
|
||||||
|
ATTR_VALUE
|
||||||
|
],
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION: item["TotalLiquid"][ATTR_VALUE],
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item[
|
||||||
|
"PrecipitationProbability"
|
||||||
|
],
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED: item["Wind"][ATTR_SPEED][ATTR_VALUE],
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGust"][ATTR_SPEED][
|
||||||
|
ATTR_VALUE
|
||||||
|
],
|
||||||
|
ATTR_FORECAST_UV_INDEX: item["UVIndex"],
|
||||||
|
ATTR_FORECAST_WIND_BEARING: item["Wind"][ATTR_DIRECTION]["Degrees"],
|
||||||
|
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["WeatherIcon"]),
|
||||||
|
}
|
||||||
|
for item in self.hourly_coordinator.data
|
||||||
|
]
|
||||||
|
@@ -3,10 +3,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
@@ -28,7 +26,6 @@ from .const import (
|
|||||||
ATTR_STRUCTURE,
|
ATTR_STRUCTURE,
|
||||||
ATTR_TASK_NAME,
|
ATTR_TASK_NAME,
|
||||||
DATA_COMPONENT,
|
DATA_COMPONENT,
|
||||||
DATA_IMAGES,
|
|
||||||
DATA_PREFERENCES,
|
DATA_PREFERENCES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_GENERATE_DATA,
|
SERVICE_GENERATE_DATA,
|
||||||
@@ -42,7 +39,6 @@ from .task import (
|
|||||||
GenDataTaskResult,
|
GenDataTaskResult,
|
||||||
GenImageTask,
|
GenImageTask,
|
||||||
GenImageTaskResult,
|
GenImageTaskResult,
|
||||||
ImageData,
|
|
||||||
async_generate_data,
|
async_generate_data,
|
||||||
async_generate_image,
|
async_generate_image,
|
||||||
)
|
)
|
||||||
@@ -55,7 +51,6 @@ __all__ = [
|
|||||||
"GenDataTaskResult",
|
"GenDataTaskResult",
|
||||||
"GenImageTask",
|
"GenImageTask",
|
||||||
"GenImageTaskResult",
|
"GenImageTaskResult",
|
||||||
"ImageData",
|
|
||||||
"async_generate_data",
|
"async_generate_data",
|
||||||
"async_generate_image",
|
"async_generate_image",
|
||||||
"async_setup",
|
"async_setup",
|
||||||
@@ -94,10 +89,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||||
hass.data[DATA_COMPONENT] = entity_component
|
hass.data[DATA_COMPONENT] = entity_component
|
||||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||||
hass.data[DATA_IMAGES] = {}
|
|
||||||
await hass.data[DATA_PREFERENCES].async_load()
|
await hass.data[DATA_PREFERENCES].async_load()
|
||||||
async_setup_http(hass)
|
async_setup_http(hass)
|
||||||
hass.http.register_view(ImageView)
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_GENERATE_DATA,
|
SERVICE_GENERATE_DATA,
|
||||||
@@ -126,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
schema=vol.Schema(
|
schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
@@ -163,9 +156,10 @@ async def async_service_generate_image(call: ServiceCall) -> ServiceResponse:
|
|||||||
class AITaskPreferences:
|
class AITaskPreferences:
|
||||||
"""AI Task preferences."""
|
"""AI Task preferences."""
|
||||||
|
|
||||||
KEYS = ("gen_data_entity_id",)
|
KEYS = ("gen_data_entity_id", "gen_image_entity_id")
|
||||||
|
|
||||||
gen_data_entity_id: str | None = None
|
gen_data_entity_id: str | None = None
|
||||||
|
gen_image_entity_id: str | None = None
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the preferences."""
|
"""Initialize the preferences."""
|
||||||
@@ -179,17 +173,21 @@ class AITaskPreferences:
|
|||||||
if data is None:
|
if data is None:
|
||||||
return
|
return
|
||||||
for key in self.KEYS:
|
for key in self.KEYS:
|
||||||
setattr(self, key, data[key])
|
setattr(self, key, data.get(key))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_set_preferences(
|
def async_set_preferences(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
gen_data_entity_id: str | None | UndefinedType = UNDEFINED,
|
gen_data_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||||
|
gen_image_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the preferences."""
|
"""Set the preferences."""
|
||||||
changed = False
|
changed = False
|
||||||
for key, value in (("gen_data_entity_id", gen_data_entity_id),):
|
for key, value in (
|
||||||
|
("gen_data_entity_id", gen_data_entity_id),
|
||||||
|
("gen_image_entity_id", gen_image_entity_id),
|
||||||
|
):
|
||||||
if value is not UNDEFINED:
|
if value is not UNDEFINED:
|
||||||
if getattr(self, key) != value:
|
if getattr(self, key) != value:
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
@@ -204,29 +202,3 @@ class AITaskPreferences:
|
|||||||
def as_dict(self) -> dict[str, str | None]:
|
def as_dict(self) -> dict[str, str | None]:
|
||||||
"""Get the current preferences."""
|
"""Get the current preferences."""
|
||||||
return {key: getattr(self, key) for key in self.KEYS}
|
return {key: getattr(self, key) for key in self.KEYS}
|
||||||
|
|
||||||
|
|
||||||
class ImageView(HomeAssistantView):
|
|
||||||
"""View to generated images."""
|
|
||||||
|
|
||||||
url = f"/api/{DOMAIN}/images/{{filename}}"
|
|
||||||
name = f"api:{DOMAIN}/images"
|
|
||||||
requires_auth = False
|
|
||||||
|
|
||||||
async def get(
|
|
||||||
self,
|
|
||||||
request: web.Request,
|
|
||||||
filename: str,
|
|
||||||
) -> web.Response:
|
|
||||||
"""Serve image."""
|
|
||||||
hass = request.app[KEY_HASS]
|
|
||||||
image_storage = hass.data[DATA_IMAGES]
|
|
||||||
image_data = image_storage.get(filename)
|
|
||||||
|
|
||||||
if image_data is None:
|
|
||||||
raise web.HTTPNotFound
|
|
||||||
|
|
||||||
return web.Response(
|
|
||||||
body=image_data.data,
|
|
||||||
content_type=image_data.mime_type,
|
|
||||||
)
|
|
||||||
|
@@ -8,19 +8,19 @@ from typing import TYPE_CHECKING, Final
|
|||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.components.media_source import local_source
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
from . import AITaskPreferences
|
from . import AITaskPreferences
|
||||||
from .entity import AITaskEntity
|
from .entity import AITaskEntity
|
||||||
from .task import ImageData
|
|
||||||
|
|
||||||
DOMAIN = "ai_task"
|
DOMAIN = "ai_task"
|
||||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||||
DATA_IMAGES: HassKey[dict[str, ImageData]] = HassKey(f"{DOMAIN}_images")
|
DATA_MEDIA_SOURCE: HassKey[local_source.LocalSource] = HassKey(f"{DOMAIN}_media_source")
|
||||||
|
|
||||||
|
IMAGE_DIR: Final = "image"
|
||||||
IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
|
IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
|
||||||
MAX_IMAGES = 20
|
|
||||||
|
|
||||||
SERVICE_GENERATE_DATA = "generate_data"
|
SERVICE_GENERATE_DATA = "generate_data"
|
||||||
SERVICE_GENERATE_IMAGE = "generate_image"
|
SERVICE_GENERATE_IMAGE = "generate_image"
|
||||||
|
@@ -60,6 +60,10 @@ class AITaskEntity(RestoreEntity):
|
|||||||
task: GenDataTask | GenImageTask,
|
task: GenDataTask | GenImageTask,
|
||||||
) -> AsyncGenerator[ChatLog]:
|
) -> AsyncGenerator[ChatLog]:
|
||||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||||
|
user_llm_hass_api: llm.API | None = None
|
||||||
|
if isinstance(task, GenDataTask):
|
||||||
|
user_llm_hass_api = task.llm_api
|
||||||
|
|
||||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||||
with (
|
with (
|
||||||
async_get_chat_log(
|
async_get_chat_log(
|
||||||
@@ -77,6 +81,7 @@ class AITaskEntity(RestoreEntity):
|
|||||||
device_id=None,
|
device_id=None,
|
||||||
),
|
),
|
||||||
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
||||||
|
user_llm_hass_api=user_llm_hass_api,
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_log.async_add_user_content(
|
chat_log.async_add_user_content(
|
||||||
|
@@ -37,6 +37,7 @@ def websocket_get_preferences(
|
|||||||
{
|
{
|
||||||
vol.Required("type"): "ai_task/preferences/set",
|
vol.Required("type"): "ai_task/preferences/set",
|
||||||
vol.Optional("gen_data_entity_id"): vol.Any(str, None),
|
vol.Optional("gen_data_entity_id"): vol.Any(str, None),
|
||||||
|
vol.Optional("gen_image_entity_id"): vol.Any(str, None),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "ai_task",
|
"domain": "ai_task",
|
||||||
"name": "AI Task",
|
"name": "AI Task",
|
||||||
"after_dependencies": ["camera", "http"],
|
"after_dependencies": ["camera"],
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["conversation", "media_source"],
|
"dependencies": ["conversation", "media_source"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||||
|
@@ -2,80 +2,31 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
from pathlib import Path
|
||||||
|
|
||||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
from homeassistant.components.media_source import MediaSource, local_source
|
||||||
from homeassistant.components.media_source import (
|
|
||||||
BrowseMediaSource,
|
|
||||||
MediaSource,
|
|
||||||
MediaSourceItem,
|
|
||||||
PlayMedia,
|
|
||||||
Unresolvable,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from .const import DATA_IMAGES, DOMAIN
|
from .const import DATA_MEDIA_SOURCE, DOMAIN, IMAGE_DIR
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_media_source(hass: HomeAssistant) -> ImageMediaSource:
|
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||||
"""Set up image media source."""
|
"""Set up local media source."""
|
||||||
_LOGGER.debug("Setting up image media source")
|
media_dirs = list(hass.config.media_dirs.values())
|
||||||
return ImageMediaSource(hass)
|
|
||||||
|
|
||||||
|
if not media_dirs:
|
||||||
class ImageMediaSource(MediaSource):
|
raise HomeAssistantError(
|
||||||
"""Provide images as media sources."""
|
"AI Task media source requires at least one media directory configured"
|
||||||
|
|
||||||
name: str = "AI Generated Images"
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
|
||||||
"""Initialize ImageMediaSource."""
|
|
||||||
super().__init__(DOMAIN)
|
|
||||||
self.hass = hass
|
|
||||||
|
|
||||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
|
||||||
"""Resolve media to a url."""
|
|
||||||
image_storage = self.hass.data[DATA_IMAGES]
|
|
||||||
image = image_storage.get(item.identifier)
|
|
||||||
|
|
||||||
if image is None:
|
|
||||||
raise Unresolvable(f"Could not resolve media item: {item.identifier}")
|
|
||||||
|
|
||||||
return PlayMedia(f"/api/{DOMAIN}/images/{item.identifier}", image.mime_type)
|
|
||||||
|
|
||||||
async def async_browse_media(
|
|
||||||
self,
|
|
||||||
item: MediaSourceItem,
|
|
||||||
) -> BrowseMediaSource:
|
|
||||||
"""Return media."""
|
|
||||||
if item.identifier:
|
|
||||||
raise BrowseError("Unknown item")
|
|
||||||
|
|
||||||
image_storage = self.hass.data[DATA_IMAGES]
|
|
||||||
|
|
||||||
children = [
|
|
||||||
BrowseMediaSource(
|
|
||||||
domain=DOMAIN,
|
|
||||||
identifier=filename,
|
|
||||||
media_class=MediaClass.IMAGE,
|
|
||||||
media_content_type=image.mime_type,
|
|
||||||
title=image.title or filename,
|
|
||||||
can_play=True,
|
|
||||||
can_expand=False,
|
|
||||||
)
|
|
||||||
for filename, image in image_storage.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
return BrowseMediaSource(
|
|
||||||
domain=DOMAIN,
|
|
||||||
identifier=None,
|
|
||||||
media_class=MediaClass.APP,
|
|
||||||
media_content_type="",
|
|
||||||
title="AI Generated Images",
|
|
||||||
can_play=False,
|
|
||||||
can_expand=True,
|
|
||||||
children_media_class=MediaClass.IMAGE,
|
|
||||||
children=children,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
media_dir = Path(media_dirs[0]) / DOMAIN / IMAGE_DIR
|
||||||
|
|
||||||
|
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"AI Generated Images",
|
||||||
|
{IMAGE_DIR: str(media_dir)},
|
||||||
|
f"/{DOMAIN}",
|
||||||
|
)
|
||||||
|
return source
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -12,33 +12,33 @@ from typing import Any
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import camera, conversation, media_source
|
from homeassistant.components import camera, conversation, image, media_source
|
||||||
|
from homeassistant.components.http.auth import async_sign_path
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import llm
|
||||||
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
|
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
|
||||||
from homeassistant.helpers.event import async_call_later
|
|
||||||
from homeassistant.helpers.network import get_url
|
|
||||||
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
|
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DATA_COMPONENT,
|
DATA_COMPONENT,
|
||||||
DATA_IMAGES,
|
DATA_MEDIA_SOURCE,
|
||||||
DATA_PREFERENCES,
|
DATA_PREFERENCES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
IMAGE_DIR,
|
||||||
IMAGE_EXPIRY_TIME,
|
IMAGE_EXPIRY_TIME,
|
||||||
MAX_IMAGES,
|
|
||||||
AITaskEntityFeature,
|
AITaskEntityFeature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _save_camera_snapshot(image: camera.Image) -> Path:
|
def _save_camera_snapshot(image_data: camera.Image | image.Image) -> Path:
|
||||||
"""Save camera snapshot to temp file."""
|
"""Save camera snapshot to temp file."""
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
mode="wb",
|
mode="wb",
|
||||||
suffix=mimetypes.guess_extension(image.content_type, False),
|
suffix=mimetypes.guess_extension(image_data.content_type, False),
|
||||||
delete=False,
|
delete=False,
|
||||||
) as temp_file:
|
) as temp_file:
|
||||||
temp_file.write(image.content)
|
temp_file.write(image_data.content)
|
||||||
return Path(temp_file.name)
|
return Path(temp_file.name)
|
||||||
|
|
||||||
|
|
||||||
@@ -54,26 +54,31 @@ async def _resolve_attachments(
|
|||||||
for attachment in attachments or []:
|
for attachment in attachments or []:
|
||||||
media_content_id = attachment["media_content_id"]
|
media_content_id = attachment["media_content_id"]
|
||||||
|
|
||||||
# Special case for camera media sources
|
# Special case for certain media sources
|
||||||
if media_content_id.startswith("media-source://camera/"):
|
for integration in camera, image:
|
||||||
# Extract entity_id from the media content ID
|
media_source_prefix = f"media-source://{integration.DOMAIN}/"
|
||||||
entity_id = media_content_id.removeprefix("media-source://camera/")
|
if not media_content_id.startswith(media_source_prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
# Get snapshot from camera
|
# Extract entity_id from the media content ID
|
||||||
image = await camera.async_get_image(hass, entity_id)
|
entity_id = media_content_id.removeprefix(media_source_prefix)
|
||||||
|
|
||||||
|
# Get snapshot from entity
|
||||||
|
image_data = await integration.async_get_image(hass, entity_id)
|
||||||
|
|
||||||
temp_filename = await hass.async_add_executor_job(
|
temp_filename = await hass.async_add_executor_job(
|
||||||
_save_camera_snapshot, image
|
_save_camera_snapshot, image_data
|
||||||
)
|
)
|
||||||
created_files.append(temp_filename)
|
created_files.append(temp_filename)
|
||||||
|
|
||||||
resolved_attachments.append(
|
resolved_attachments.append(
|
||||||
conversation.Attachment(
|
conversation.Attachment(
|
||||||
media_content_id=media_content_id,
|
media_content_id=media_content_id,
|
||||||
mime_type=image.content_type,
|
mime_type=image_data.content_type,
|
||||||
path=temp_filename,
|
path=temp_filename,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
# Handle regular media sources
|
# Handle regular media sources
|
||||||
media = await media_source.async_resolve_media(hass, media_content_id, None)
|
media = await media_source.async_resolve_media(hass, media_content_id, None)
|
||||||
@@ -115,6 +120,7 @@ async def async_generate_data(
|
|||||||
instructions: str,
|
instructions: str,
|
||||||
structure: vol.Schema | None = None,
|
structure: vol.Schema | None = None,
|
||||||
attachments: list[dict] | None = None,
|
attachments: list[dict] | None = None,
|
||||||
|
llm_api: llm.API | None = None,
|
||||||
) -> GenDataTaskResult:
|
) -> GenDataTaskResult:
|
||||||
"""Run a data generation task in the AI Task integration."""
|
"""Run a data generation task in the AI Task integration."""
|
||||||
if entity_id is None:
|
if entity_id is None:
|
||||||
@@ -150,37 +156,26 @@ async def async_generate_data(
|
|||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
structure=structure,
|
structure=structure,
|
||||||
attachments=resolved_attachments or None,
|
attachments=resolved_attachments or None,
|
||||||
|
llm_api=llm_api,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_images(image_storage: dict[str, ImageData], num_to_remove: int) -> None:
|
|
||||||
"""Remove old images to keep the storage size under the limit."""
|
|
||||||
if num_to_remove <= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if num_to_remove >= len(image_storage):
|
|
||||||
image_storage.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
sorted_images = sorted(
|
|
||||||
image_storage.items(),
|
|
||||||
key=lambda item: item[1].timestamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
for filename, _ in sorted_images[:num_to_remove]:
|
|
||||||
image_storage.pop(filename, None)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_generate_image(
|
async def async_generate_image(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
*,
|
*,
|
||||||
task_name: str,
|
task_name: str,
|
||||||
entity_id: str,
|
entity_id: str | None = None,
|
||||||
instructions: str,
|
instructions: str,
|
||||||
attachments: list[dict] | None = None,
|
attachments: list[dict] | None = None,
|
||||||
) -> ServiceResponse:
|
) -> ServiceResponse:
|
||||||
"""Run an image generation task in the AI Task integration."""
|
"""Run an image generation task in the AI Task integration."""
|
||||||
|
if entity_id is None:
|
||||||
|
entity_id = hass.data[DATA_PREFERENCES].gen_image_entity_id
|
||||||
|
|
||||||
|
if entity_id is None:
|
||||||
|
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||||
|
|
||||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||||
@@ -215,32 +210,34 @@ async def async_generate_image(
|
|||||||
if service_result.get("revised_prompt") is None:
|
if service_result.get("revised_prompt") is None:
|
||||||
service_result["revised_prompt"] = instructions
|
service_result["revised_prompt"] = instructions
|
||||||
|
|
||||||
image_storage = hass.data[DATA_IMAGES]
|
source = hass.data[DATA_MEDIA_SOURCE]
|
||||||
|
|
||||||
if len(image_storage) + 1 > MAX_IMAGES:
|
|
||||||
_cleanup_images(image_storage, len(image_storage) + 1 - MAX_IMAGES)
|
|
||||||
|
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
|
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
|
||||||
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
|
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
|
||||||
filename = f"{current_time.strftime('%Y-%m-%d_%H%M%S')}_{sanitized_task_name}{ext}"
|
|
||||||
|
|
||||||
image_storage[filename] = ImageData(
|
image_file = ImageData(
|
||||||
data=image_data,
|
filename=f"{current_time.strftime('%Y-%m-%d_%H%M%S')}_{sanitized_task_name}{ext}",
|
||||||
timestamp=int(current_time.timestamp()),
|
file=io.BytesIO(image_data),
|
||||||
mime_type=task_result.mime_type,
|
content_type=task_result.mime_type,
|
||||||
title=service_result["revised_prompt"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _purge_image(filename: str, now: datetime) -> None:
|
target_folder = media_source.MediaSourceItem.from_uri(
|
||||||
"""Remove image from storage."""
|
hass, f"media-source://{DOMAIN}/{IMAGE_DIR}", None
|
||||||
image_storage.pop(filename, None)
|
)
|
||||||
|
|
||||||
if IMAGE_EXPIRY_TIME > 0:
|
service_result["media_source_id"] = await source.async_upload_media(
|
||||||
async_call_later(hass, IMAGE_EXPIRY_TIME, partial(_purge_image, filename))
|
target_folder, image_file
|
||||||
|
)
|
||||||
|
|
||||||
service_result["url"] = get_url(hass) + f"/api/{DOMAIN}/images/{filename}"
|
item = media_source.MediaSourceItem.from_uri(
|
||||||
service_result["media_source_id"] = f"media-source://{DOMAIN}/images/{filename}"
|
hass, service_result["media_source_id"], None
|
||||||
|
)
|
||||||
|
service_result["url"] = async_sign_path(
|
||||||
|
hass,
|
||||||
|
(await source.async_resolve_media(item)).url,
|
||||||
|
timedelta(seconds=IMAGE_EXPIRY_TIME),
|
||||||
|
)
|
||||||
|
|
||||||
return service_result
|
return service_result
|
||||||
|
|
||||||
@@ -261,6 +258,9 @@ class GenDataTask:
|
|||||||
attachments: list[conversation.Attachment] | None = None
|
attachments: list[conversation.Attachment] | None = None
|
||||||
"""List of attachments to go along the instructions."""
|
"""List of attachments to go along the instructions."""
|
||||||
|
|
||||||
|
llm_api: llm.API | None = None
|
||||||
|
"""API to provide to the LLM."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return task as a string."""
|
"""Return task as a string."""
|
||||||
return f"<GenDataTask {self.name}: {id(self)}>"
|
return f"<GenDataTask {self.name}: {id(self)}>"
|
||||||
@@ -342,20 +342,8 @@ class GenImageTaskResult:
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class ImageData:
|
class ImageData:
|
||||||
"""Image data for stored generated images."""
|
"""Implementation of media_source.local_source.UploadedFile protocol."""
|
||||||
|
|
||||||
data: bytes
|
filename: str
|
||||||
"""Raw image data."""
|
file: io.IOBase
|
||||||
|
content_type: str
|
||||||
timestamp: int
|
|
||||||
"""Timestamp when the image was generated, as a Unix timestamp."""
|
|
||||||
|
|
||||||
mime_type: str
|
|
||||||
"""MIME type of the image."""
|
|
||||||
|
|
||||||
title: str
|
|
||||||
"""Title of the image, usually the prompt used to generate it."""
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return image data as a string."""
|
|
||||||
return f"<ImageData {self.title}: {id(self)}>"
|
|
||||||
|
@@ -2,12 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from airos.airos8 import AirOS
|
from airos.airos8 import AirOS8
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
|
||||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
_PLATFORMS: list[Platform] = [
|
_PLATFORMS: list[Platform] = [
|
||||||
@@ -21,13 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
|
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(hass, verify_ssl=False)
|
session = async_get_clientsession(
|
||||||
|
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
|
||||||
|
)
|
||||||
|
|
||||||
airos_device = AirOS(
|
airos_device = AirOS8(
|
||||||
host=entry.data[CONF_HOST],
|
host=entry.data[CONF_HOST],
|
||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
session=session,
|
session=session,
|
||||||
|
use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||||
@@ -40,6 +51,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
|
"""Migrate old config entry."""
|
||||||
|
|
||||||
|
if entry.version > 1:
|
||||||
|
# This means the user has downgraded from a future version
|
||||||
|
return False
|
||||||
|
|
||||||
|
if entry.version == 1 and entry.minor_version == 1:
|
||||||
|
new_data = {**entry.data}
|
||||||
|
advanced_data = {
|
||||||
|
CONF_SSL: DEFAULT_SSL,
|
||||||
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
||||||
|
}
|
||||||
|
new_data[SECTION_ADVANCED_SETTINGS] = advanced_data
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data=new_data,
|
||||||
|
minor_version=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
|
@@ -15,7 +15,7 @@ from homeassistant.const import EntityCategory
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
from .entity import AirOSEntity
|
from .entity import AirOSEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -27,7 +27,7 @@ PARALLEL_UPDATES = 0
|
|||||||
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""Describe an AirOS binary sensor."""
|
"""Describe an AirOS binary sensor."""
|
||||||
|
|
||||||
value_fn: Callable[[AirOSData], bool]
|
value_fn: Callable[[AirOS8Data], bool]
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
|
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -14,12 +15,24 @@ from airos.exceptions import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.data_entry_flow import section
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
TextSelectorType,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
||||||
from .coordinator import AirOS
|
from .coordinator import AirOS8
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,6 +41,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_HOST): str,
|
vol.Required(CONF_HOST): str,
|
||||||
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||||
|
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{"collapsed": True},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,47 +58,109 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Ubiquiti airOS."""
|
"""Handle a config flow for Ubiquiti airOS."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
super().__init__()
|
||||||
|
self.airos_device: AirOS8
|
||||||
|
self.errors: dict[str, str] = {}
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self,
|
self, user_input: dict[str, Any] | None = None
|
||||||
user_input: dict[str, Any] | None = None,
|
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the manual input of host and credentials."""
|
||||||
errors: dict[str, str] = {}
|
self.errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
validated_info = await self._validate_and_get_device_info(user_input)
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
if validated_info:
|
||||||
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(
|
return self.async_create_entry(
|
||||||
title=airos_data.host.hostname, data=user_input
|
title=validated_info["title"],
|
||||||
|
data=validated_info["data"],
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _validate_and_get_device_info(
|
||||||
|
self, config_data: dict[str, Any]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Validate user input with the device API."""
|
||||||
|
# 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=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||||
|
)
|
||||||
|
|
||||||
|
airos_device = AirOS8(
|
||||||
|
host=config_data[CONF_HOST],
|
||||||
|
username=config_data[CONF_USERNAME],
|
||||||
|
password=config_data[CONF_PASSWORD],
|
||||||
|
session=session,
|
||||||
|
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await airos_device.login()
|
||||||
|
airos_data = await airos_device.status()
|
||||||
|
|
||||||
|
except (
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
):
|
||||||
|
self.errors["base"] = "cannot_connect"
|
||||||
|
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||||
|
self.errors["base"] = "invalid_auth"
|
||||||
|
except AirOSKeyDataMissingError:
|
||||||
|
self.errors["base"] = "key_data_missing"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception during credential validation")
|
||||||
|
self.errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(airos_data.derived.mac)
|
||||||
|
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
else:
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return {"title": airos_data.host.hostname, "data": config_data}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self,
|
||||||
|
user_input: Mapping[str, Any],
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauthentication upon an API authentication error."""
|
||||||
|
return await self.async_step_reauth_confirm(user_input)
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self,
|
||||||
|
user_input: Mapping[str, Any],
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauthentication upon an API authentication error."""
|
||||||
|
self.errors = {}
|
||||||
|
|
||||||
|
if user_input:
|
||||||
|
validate_data = {**self._get_reauth_entry().data, **user_input}
|
||||||
|
if await self._validate_and_get_device_info(config_data=validate_data):
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(),
|
||||||
|
data_updates=validate_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PASSWORD): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.PASSWORD,
|
||||||
|
autocomplete="current-password",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=self.errors,
|
||||||
)
|
)
|
||||||
|
@@ -7,3 +7,8 @@ DOMAIN = "airos"
|
|||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
MANUFACTURER = "Ubiquiti"
|
MANUFACTURER = "Ubiquiti"
|
||||||
|
|
||||||
|
DEFAULT_VERIFY_SSL = False
|
||||||
|
DEFAULT_SSL = True
|
||||||
|
|
||||||
|
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from airos.airos8 import AirOS, AirOSData
|
from airos.airos8 import AirOS8, AirOS8Data
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
AirOSConnectionAuthenticationError,
|
AirOSConnectionAuthenticationError,
|
||||||
AirOSConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
@@ -14,7 +14,7 @@ from airos.exceptions import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryError
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, SCAN_INTERVAL
|
from .const import DOMAIN, SCAN_INTERVAL
|
||||||
@@ -24,13 +24,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
||||||
"""Class to manage fetching AirOS data from single endpoint."""
|
"""Class to manage fetching AirOS data from single endpoint."""
|
||||||
|
|
||||||
config_entry: AirOSConfigEntry
|
config_entry: AirOSConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS8
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
self.airos_device = airos_device
|
self.airos_device = airos_device
|
||||||
@@ -42,14 +42,14 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
|||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> AirOSData:
|
async def _async_update_data(self) -> AirOS8Data:
|
||||||
"""Fetch data from AirOS."""
|
"""Fetch data from AirOS."""
|
||||||
try:
|
try:
|
||||||
await self.airos_device.login()
|
await self.airos_device.login()
|
||||||
return await self.airos_device.status()
|
return await self.airos_device.status()
|
||||||
except (AirOSConnectionAuthenticationError,) as err:
|
except AirOSConnectionAuthenticationError as err:
|
||||||
_LOGGER.exception("Error authenticating with airOS device")
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
) from err
|
) from err
|
||||||
except (
|
except (
|
||||||
|
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST, CONF_SSL
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER
|
from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS
|
||||||
from .coordinator import AirOSDataUpdateCoordinator
|
from .coordinator import AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +20,14 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
airos_data = self.coordinator.data
|
airos_data = self.coordinator.data
|
||||||
|
url_schema = (
|
||||||
|
"https"
|
||||||
|
if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
||||||
|
else "http"
|
||||||
|
)
|
||||||
|
|
||||||
configuration_url: str | None = (
|
configuration_url: str | None = (
|
||||||
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
f"{url_schema}://{coordinator.config_entry.data[CONF_HOST]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.4.4"]
|
"requirements": ["airos==0.5.5"]
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
from .entity import AirOSEntity
|
from .entity import AirOSEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -42,7 +42,7 @@ PARALLEL_UPDATES = 0
|
|||||||
class AirOSSensorEntityDescription(SensorEntityDescription):
|
class AirOSSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describe an AirOS sensor."""
|
"""Describe an AirOS sensor."""
|
||||||
|
|
||||||
value_fn: Callable[[AirOSData], StateType]
|
value_fn: Callable[[AirOS8Data], StateType]
|
||||||
|
|
||||||
|
|
||||||
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||||
|
@@ -2,6 +2,14 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"flow_title": "Ubiquiti airOS device",
|
"flow_title": "Ubiquiti airOS device",
|
||||||
"step": {
|
"step": {
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "[%key:component::airos::config::step::user::data_description::password%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
@@ -12,6 +20,18 @@
|
|||||||
"host": "IP address or hostname of the airOS device",
|
"host": "IP address or hostname of the airOS device",
|
||||||
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||||
"password": "Password configured through the UISP app or web interface"
|
"password": "Password configured through the UISP app or web interface"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"advanced_settings": {
|
||||||
|
"data": {
|
||||||
|
"ssl": "Use HTTPS",
|
||||||
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"ssl": "Whether the connection should be encrypted (required for most devices)",
|
||||||
|
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -22,7 +42,9 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
@@ -23,6 +23,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
URL_API_INTEGRATION = {
|
||||||
|
"url": "https://dashboard.airthings.com/integrations/api-integration"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Airthings."""
|
"""Handle a config flow for Airthings."""
|
||||||
@@ -37,11 +41,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
description_placeholders={
|
description_placeholders=URL_API_INTEGRATION,
|
||||||
"url": (
|
|
||||||
"https://dashboard.airthings.com/integrations/api-integration"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
@@ -65,5 +65,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(title="Airthings", data=user_input)
|
return self.async_create_entry(title="Airthings", data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders=URL_API_INTEGRATION,
|
||||||
)
|
)
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"secret": "Secret",
|
"secret": "Secret"
|
||||||
"description": "Login at {url} to find your credentials"
|
},
|
||||||
}
|
"description": "Log in at {url} to find your credentials"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
@@ -6,8 +6,13 @@ import dataclasses
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
from airthings_ble import (
|
||||||
|
AirthingsBluetoothDeviceData,
|
||||||
|
AirthingsDevice,
|
||||||
|
UnsupportedDeviceError,
|
||||||
|
)
|
||||||
from bleak import BleakError
|
from bleak import BleakError
|
||||||
|
from habluetooth import BluetoothServiceInfoBleak
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
@@ -27,6 +32,7 @@ SERVICE_UUIDS = [
|
|||||||
"b42e4a8e-ade7-11e4-89d3-123b93f75cba",
|
"b42e4a8e-ade7-11e4-89d3-123b93f75cba",
|
||||||
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
|
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
|
||||||
"b42e3882-ade7-11e4-89d3-123b93f75cba",
|
"b42e3882-ade7-11e4-89d3-123b93f75cba",
|
||||||
|
"b42e90a2-ade7-11e4-89d3-123b93f75cba",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ class Discovery:
|
|||||||
name: str
|
name: str
|
||||||
discovery_info: BluetoothServiceInfo
|
discovery_info: BluetoothServiceInfo
|
||||||
device: AirthingsDevice
|
device: AirthingsDevice
|
||||||
|
data: AirthingsBluetoothDeviceData
|
||||||
|
|
||||||
|
|
||||||
def get_name(device: AirthingsDevice) -> str:
|
def get_name(device: AirthingsDevice) -> str:
|
||||||
@@ -44,7 +51,7 @@ def get_name(device: AirthingsDevice) -> str:
|
|||||||
|
|
||||||
name = device.friendly_name()
|
name = device.friendly_name()
|
||||||
if identifier := device.identifier:
|
if identifier := device.identifier:
|
||||||
name += f" ({identifier})"
|
name += f" ({device.model.value}{identifier})"
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
@@ -62,8 +69,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._discovered_device: Discovery | None = None
|
self._discovered_device: Discovery | None = None
|
||||||
self._discovered_devices: dict[str, Discovery] = {}
|
self._discovered_devices: dict[str, Discovery] = {}
|
||||||
|
|
||||||
async def _get_device_data(
|
async def _get_device(
|
||||||
self, discovery_info: BluetoothServiceInfo
|
self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo
|
||||||
) -> AirthingsDevice:
|
) -> AirthingsDevice:
|
||||||
ble_device = bluetooth.async_ble_device_from_address(
|
ble_device = bluetooth.async_ble_device_from_address(
|
||||||
self.hass, discovery_info.address
|
self.hass, discovery_info.address
|
||||||
@@ -72,10 +79,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("no ble_device in _get_device_data")
|
_LOGGER.debug("no ble_device in _get_device_data")
|
||||||
raise AirthingsDeviceUpdateError("No ble_device")
|
raise AirthingsDeviceUpdateError("No ble_device")
|
||||||
|
|
||||||
airthings = AirthingsBluetoothDeviceData(_LOGGER)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await airthings.update_device(ble_device)
|
device = await data.update_device(ble_device)
|
||||||
except BleakError as err:
|
except BleakError as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Error connecting to and getting data from %s: %s",
|
"Error connecting to and getting data from %s: %s",
|
||||||
@@ -83,12 +88,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
raise AirthingsDeviceUpdateError("Failed getting device data") from err
|
raise AirthingsDeviceUpdateError("Failed getting device data") from err
|
||||||
|
except UnsupportedDeviceError:
|
||||||
|
_LOGGER.debug("Skipping unsupported device: %s", discovery_info.name)
|
||||||
|
raise
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unknown error occurred from %s: %s", discovery_info.address, err
|
"Unknown error occurred from %s: %s", discovery_info.address, err
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
return data
|
return device
|
||||||
|
|
||||||
async def async_step_bluetooth(
|
async def async_step_bluetooth(
|
||||||
self, discovery_info: BluetoothServiceInfo
|
self, discovery_info: BluetoothServiceInfo
|
||||||
@@ -98,17 +106,21 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.async_set_unique_id(discovery_info.address)
|
await self.async_set_unique_id(discovery_info.address)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
device = await self._get_device_data(discovery_info)
|
device = await self._get_device(data=data, discovery_info=discovery_info)
|
||||||
except AirthingsDeviceUpdateError:
|
except AirthingsDeviceUpdateError:
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
except UnsupportedDeviceError:
|
||||||
|
return self.async_abort(reason="unsupported_device")
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unknown error occurred")
|
_LOGGER.exception("Unknown error occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
name = get_name(device)
|
name = get_name(device)
|
||||||
self.context["title_placeholders"] = {"name": name}
|
self.context["title_placeholders"] = {"name": name}
|
||||||
self._discovered_device = Discovery(name, discovery_info, device)
|
self._discovered_device = Discovery(name, discovery_info, device, data=data)
|
||||||
|
|
||||||
return await self.async_step_bluetooth_confirm()
|
return await self.async_step_bluetooth_confirm()
|
||||||
|
|
||||||
@@ -117,6 +129,12 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Confirm discovery."""
|
"""Confirm discovery."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
if (
|
||||||
|
self._discovered_device is not None
|
||||||
|
and self._discovered_device.device.firmware.need_firmware_upgrade
|
||||||
|
):
|
||||||
|
return self.async_abort(reason="firmware_upgrade_required")
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self.context["title_placeholders"]["name"], data={}
|
title=self.context["title_placeholders"]["name"], data={}
|
||||||
)
|
)
|
||||||
@@ -137,6 +155,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
discovery = self._discovered_devices[address]
|
discovery = self._discovered_devices[address]
|
||||||
|
|
||||||
|
if discovery.device.firmware.need_firmware_upgrade:
|
||||||
|
return self.async_abort(reason="firmware_upgrade_required")
|
||||||
|
|
||||||
self.context["title_placeholders"] = {
|
self.context["title_placeholders"] = {
|
||||||
"name": discovery.name,
|
"name": discovery.name,
|
||||||
}
|
}
|
||||||
@@ -146,32 +167,53 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(title=discovery.name, data={})
|
return self.async_create_entry(title=discovery.name, data={})
|
||||||
|
|
||||||
current_addresses = self._async_current_ids(include_ignore=False)
|
current_addresses = self._async_current_ids(include_ignore=False)
|
||||||
|
devices: list[BluetoothServiceInfoBleak] = []
|
||||||
for discovery_info in async_discovered_service_info(self.hass):
|
for discovery_info in async_discovered_service_info(self.hass):
|
||||||
address = discovery_info.address
|
address = discovery_info.address
|
||||||
if address in current_addresses or address in self._discovered_devices:
|
if address in current_addresses or address in self._discovered_devices:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if MFCT_ID not in discovery_info.manufacturer_data:
|
if MFCT_ID not in discovery_info.manufacturer_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
|
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Skipping unsupported device: %s (%s)", discovery_info.name, address
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
devices.append(discovery_info)
|
||||||
|
|
||||||
|
for discovery_info in devices:
|
||||||
|
address = discovery_info.address
|
||||||
|
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
|
||||||
try:
|
try:
|
||||||
device = await self._get_device_data(discovery_info)
|
device = await self._get_device(data, discovery_info)
|
||||||
except AirthingsDeviceUpdateError:
|
except AirthingsDeviceUpdateError:
|
||||||
return self.async_abort(reason="cannot_connect")
|
_LOGGER.error(
|
||||||
|
"Error connecting to and getting data from %s (%s)",
|
||||||
|
discovery_info.name,
|
||||||
|
discovery_info.address,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except UnsupportedDeviceError:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Skipping unsupported device: %s (%s)",
|
||||||
|
discovery_info.name,
|
||||||
|
discovery_info.address,
|
||||||
|
)
|
||||||
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unknown error occurred")
|
_LOGGER.exception("Unknown error occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
name = get_name(device)
|
name = get_name(device)
|
||||||
self._discovered_devices[address] = Discovery(name, discovery_info, device)
|
_LOGGER.debug("Discovered Airthings device: %s (%s)", name, address)
|
||||||
|
self._discovered_devices[address] = Discovery(
|
||||||
|
name, discovery_info, device, data
|
||||||
|
)
|
||||||
|
|
||||||
if not self._discovered_devices:
|
if not self._discovered_devices:
|
||||||
return self.async_abort(reason="no_devices_found")
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
|
||||||
titles = {
|
titles = {
|
||||||
address: discovery.device.name
|
address: get_name(discovery.device)
|
||||||
for (address, discovery) in self._discovered_devices.items()
|
for (address, discovery) in self._discovered_devices.items()
|
||||||
}
|
}
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@@ -17,6 +17,10 @@
|
|||||||
{
|
{
|
||||||
"manufacturer_id": 820,
|
"manufacturer_id": 820,
|
||||||
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
|
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer_id": 820,
|
||||||
|
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codeowners": ["@vincegio", "@LaStrada"],
|
"codeowners": ["@vincegio", "@LaStrada"],
|
||||||
@@ -24,5 +28,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==0.9.2"]
|
"requirements": ["airthings-ble==1.1.1"]
|
||||||
}
|
}
|
||||||
|
@@ -114,6 +114,8 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
|
def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
|
||||||
|
@@ -6,6 +6,9 @@
|
|||||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||||
"data": {
|
"data": {
|
||||||
"address": "[%key:common::config_flow::data::device%]"
|
"address": "[%key:common::config_flow::data::device%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"address": "The Airthings devices discovered via Bluetooth."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bluetooth_confirm": {
|
"bluetooth_confirm": {
|
||||||
@@ -17,6 +20,8 @@
|
|||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
|
||||||
|
"unsupported_device": "Unsupported device",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
from airtouch4pyapi import AirTouch
|
from airtouch4pyapi import AirTouch
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .coordinator import AirtouchDataUpdateCoordinator
|
from .coordinator import AirTouch4ConfigEntry, AirtouchDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.CLIMATE]
|
PLATFORMS = [Platform.CLIMATE]
|
||||||
|
|
||||||
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
|
||||||
"""Set up AirTouch4 from a config entry."""
|
"""Set up AirTouch4 from a config entry."""
|
||||||
@@ -22,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) ->
|
|||||||
info = airtouch.GetAcs()
|
info = airtouch.GetAcs()
|
||||||
if not info:
|
if not info:
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
coordinator = AirtouchDataUpdateCoordinator(hass, airtouch)
|
coordinator = AirtouchDataUpdateCoordinator(hass, entry, airtouch)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
@@ -2,26 +2,34 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from airtouch4pyapi import AirTouch
|
||||||
from airtouch4pyapi.airtouch import AirTouchStatus
|
from airtouch4pyapi.airtouch import AirTouchStatus
|
||||||
|
|
||||||
from homeassistant.components.climate import SCAN_INTERVAL
|
from homeassistant.components.climate import SCAN_INTERVAL
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
|
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching Airtouch data."""
|
"""Class to manage fetching Airtouch data."""
|
||||||
|
|
||||||
def __init__(self, hass, airtouch):
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, entry: AirTouch4ConfigEntry, airtouch: AirTouch
|
||||||
|
) -> None:
|
||||||
"""Initialize global Airtouch data updater."""
|
"""Initialize global Airtouch data updater."""
|
||||||
self.airtouch = airtouch
|
self.airtouch = airtouch
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
|
@@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==1.0.0"]
|
"requirements": ["aioairzone==1.0.1"]
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,19 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
|
from aioairzone.common import GrilleAngle, OperationMode, QAdapt, SleepTimeout
|
||||||
from aioairzone.const import (
|
from aioairzone.const import (
|
||||||
API_COLD_ANGLE,
|
API_COLD_ANGLE,
|
||||||
API_HEAT_ANGLE,
|
API_HEAT_ANGLE,
|
||||||
API_MODE,
|
API_MODE,
|
||||||
|
API_Q_ADAPT,
|
||||||
API_SLEEP,
|
API_SLEEP,
|
||||||
AZD_COLD_ANGLE,
|
AZD_COLD_ANGLE,
|
||||||
AZD_HEAT_ANGLE,
|
AZD_HEAT_ANGLE,
|
||||||
AZD_MASTER,
|
AZD_MASTER,
|
||||||
AZD_MODE,
|
AZD_MODE,
|
||||||
AZD_MODES,
|
AZD_MODES,
|
||||||
|
AZD_Q_ADAPT,
|
||||||
AZD_SLEEP,
|
AZD_SLEEP,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
)
|
)
|
||||||
@@ -65,6 +67,14 @@ SLEEP_DICT: Final[dict[str, int]] = {
|
|||||||
"90m": SleepTimeout.SLEEP_90,
|
"90m": SleepTimeout.SLEEP_90,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Q_ADAPT_DICT: Final[dict[str, int]] = {
|
||||||
|
"standard": QAdapt.STANDARD,
|
||||||
|
"power": QAdapt.POWER,
|
||||||
|
"silence": QAdapt.SILENCE,
|
||||||
|
"minimum": QAdapt.MINIMUM,
|
||||||
|
"maximum": QAdapt.MAXIMUM,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main_zone_options(
|
def main_zone_options(
|
||||||
zone_data: dict[str, Any],
|
zone_data: dict[str, Any],
|
||||||
@@ -83,6 +93,14 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
|||||||
options_fn=main_zone_options,
|
options_fn=main_zone_options,
|
||||||
translation_key="modes",
|
translation_key="modes",
|
||||||
),
|
),
|
||||||
|
AirzoneSelectDescription(
|
||||||
|
api_param=API_Q_ADAPT,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
key=AZD_Q_ADAPT,
|
||||||
|
options=list(Q_ADAPT_DICT),
|
||||||
|
options_dict=Q_ADAPT_DICT,
|
||||||
|
translation_key="q_adapt",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -63,6 +63,16 @@
|
|||||||
"stop": "Stop"
|
"stop": "Stop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"q_adapt": {
|
||||||
|
"name": "Q-Adapt",
|
||||||
|
"state": {
|
||||||
|
"standard": "Standard",
|
||||||
|
"power": "Power",
|
||||||
|
"silence": "Silence",
|
||||||
|
"minimum": "Minimum",
|
||||||
|
"maximum": "Maximum"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sleep_times": {
|
"sleep_times": {
|
||||||
"name": "Sleep",
|
"name": "Sleep",
|
||||||
"state": {
|
"state": {
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
from genie_partner_sdk.client import AladdinConnectClient
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -36,22 +35,7 @@ async def async_setup_entry(
|
|||||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||||
)
|
)
|
||||||
|
|
||||||
sdk_doors = await client.get_doors()
|
doors = await client.get_doors()
|
||||||
|
|
||||||
# Convert SDK GarageDoor objects to integration GarageDoor objects
|
|
||||||
doors = [
|
|
||||||
GarageDoor(
|
|
||||||
{
|
|
||||||
"device_id": door.device_id,
|
|
||||||
"door_number": door.door_number,
|
|
||||||
"name": door.name,
|
|
||||||
"status": door.status,
|
|
||||||
"link_status": door.link_status,
|
|
||||||
"battery_level": door.battery_level,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for door in sdk_doors
|
|
||||||
]
|
|
||||||
|
|
||||||
entry.runtime_data = {
|
entry.runtime_data = {
|
||||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
||||||
|
@@ -22,6 +22,17 @@ class OAuth2FlowHandler(
|
|||||||
VERSION = CONFIG_FLOW_VERSION
|
VERSION = CONFIG_FLOW_VERSION
|
||||||
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
|
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Check we have the cloud integration set up."""
|
||||||
|
if "cloud" not in self.hass.config.components:
|
||||||
|
return self.async_abort(
|
||||||
|
reason="cloud_not_enabled",
|
||||||
|
description_placeholders={"default_config": "default_config"},
|
||||||
|
)
|
||||||
|
return await super().async_step_user(user_input)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, user_input: Mapping[str, Any]
|
self, user_input: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
@@ -41,4 +41,10 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
|||||||
async def _async_update_data(self) -> GarageDoor:
|
async def _async_update_data(self) -> GarageDoor:
|
||||||
"""Fetch data from the Aladdin Connect API."""
|
"""Fetch data from the Aladdin Connect API."""
|
||||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
await self.client.update_door(self.data.device_id, self.data.door_number)
|
||||||
|
self.data.status = self.client.get_door_status(
|
||||||
|
self.data.device_id, self.data.door_number
|
||||||
|
)
|
||||||
|
self.data.battery_level = self.client.get_battery_status(
|
||||||
|
self.data.device_id, self.data.door_number
|
||||||
|
)
|
||||||
return self.data
|
return self.data
|
||||||
|
@@ -49,7 +49,9 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
|||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool | None:
|
def is_closed(self) -> bool | None:
|
||||||
"""Update is closed attribute."""
|
"""Update is closed attribute."""
|
||||||
return self.coordinator.data.status == "closed"
|
if (status := self.coordinator.data.status) is None:
|
||||||
|
return None
|
||||||
|
return status == "closed"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closing(self) -> bool | None:
|
def is_closing(self) -> bool | None:
|
||||||
|
@@ -4,8 +4,13 @@
|
|||||||
"codeowners": ["@swcloudgenie"],
|
"codeowners": ["@swcloudgenie"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["application_credentials"],
|
"dependencies": ["application_credentials"],
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"hostname": "gdocntl-*"
|
||||||
|
}
|
||||||
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["genie-partner-sdk==1.0.10"]
|
"requirements": ["genie-partner-sdk==1.0.11"]
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,9 @@
|
|||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"title": "[%key:common::config_flow::title::reauth%]",
|
"title": "[%key:common::config_flow::title::reauth%]",
|
||||||
"description": "Aladdin Connect needs to re-authenticate your account"
|
"description": "Aladdin Connect needs to re-authenticate your account"
|
||||||
|
},
|
||||||
|
"oauth_discovery": {
|
||||||
|
"description": "Home Assistant has found an Aladdin Connect device on your network. Press **Submit** to continue setting up Aladdin Connect."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
@@ -21,7 +24,8 @@
|
|||||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account."
|
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account.",
|
||||||
|
"cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml."
|
||||||
},
|
},
|
||||||
"create_entry": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
@@ -61,7 +61,7 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Track states and offer events for sensors."""
|
"""Set up the alarm control panel component."""
|
||||||
component = hass.data[DATA_COMPONENT] = EntityComponent[AlarmControlPanelEntity](
|
component = hass.data[DATA_COMPONENT] = EntityComponent[AlarmControlPanelEntity](
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
"""Support for repeating alerts when conditions are met."""
|
"""Support for repeating alerts when conditions are met.
|
||||||
|
|
||||||
|
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -63,7 +66,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Alert component."""
|
"""Set up the Alert component.
|
||||||
|
|
||||||
|
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||||
|
"""
|
||||||
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
entities: list[AlertEntity] = []
|
entities: list[AlertEntity] = []
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
"""Support for repeating alerts when conditions are met."""
|
"""Support for repeating alerts when conditions are met.
|
||||||
|
|
||||||
|
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -27,7 +30,10 @@ from .const import DOMAIN, LOGGER
|
|||||||
|
|
||||||
|
|
||||||
class AlertEntity(Entity):
|
class AlertEntity(Entity):
|
||||||
"""Representation of an alert."""
|
"""Representation of an alert.
|
||||||
|
|
||||||
|
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||||
|
"""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
"""Reproduce an Alert state."""
|
"""Reproduce an Alert state.
|
||||||
|
|
||||||
|
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN
|
from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN
|
||||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||||
from .services import async_setup_services
|
from .services import async_setup_services
|
||||||
|
|
||||||
@@ -42,7 +42,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
|||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||||
"""Migrate old entry."""
|
"""Migrate old entry."""
|
||||||
if entry.version == 1 and entry.minor_version == 1:
|
|
||||||
|
if entry.version == 1 and entry.minor_version < 3:
|
||||||
|
if CONF_SITE in entry.data:
|
||||||
|
# Site in data (wrong place), just move to login data
|
||||||
|
new_data = entry.data.copy()
|
||||||
|
new_data[CONF_LOGIN_DATA][CONF_SITE] = new_data[CONF_SITE]
|
||||||
|
new_data.pop(CONF_SITE)
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, data=new_data, version=1, minor_version=3
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if CONF_SITE in entry.data[CONF_LOGIN_DATA]:
|
||||||
|
# Site is there, just update version to avoid future migrations
|
||||||
|
hass.config_entries.async_update_entry(entry, version=1, minor_version=3)
|
||||||
|
return True
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
"Migrating from version %s.%s", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
@@ -53,10 +69,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) ->
|
|||||||
|
|
||||||
# Add site to login data
|
# Add site to login data
|
||||||
new_data = entry.data.copy()
|
new_data = entry.data.copy()
|
||||||
new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}"
|
new_data[CONF_LOGIN_DATA][CONF_SITE] = f"https://www.amazon.{domain}"
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry, data=new_data, version=1, minor_version=2
|
entry, data=new_data, version=1, minor_version=3
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
|
@@ -10,6 +10,7 @@ from aioamazondevices.api import AmazonDevice
|
|||||||
from aioamazondevices.const import SENSOR_STATE_OFF
|
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
@@ -17,9 +18,12 @@ from homeassistant.components.binary_sensor import (
|
|||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
|
from .const import _LOGGER, DOMAIN
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
|
from .utils import async_update_unique_id
|
||||||
|
|
||||||
# Coordinator is used to centralize the data updates
|
# Coordinator is used to centralize the data updates
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -31,6 +35,7 @@ class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
|||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice, str], bool]
|
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||||
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||||
|
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSORS: Final = (
|
BINARY_SENSORS: Final = (
|
||||||
@@ -40,47 +45,52 @@ BINARY_SENSORS: Final = (
|
|||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
is_on_fn=lambda device, _: device.online,
|
is_on_fn=lambda device, _: device.online,
|
||||||
),
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="detectionState",
|
||||||
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
|
is_on_fn=lambda device, key: bool(
|
||||||
|
device.sensors[key].value != SENSOR_STATE_OFF
|
||||||
|
),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
is_available_fn=lambda device, key: (
|
||||||
|
device.online
|
||||||
|
and (sensor := device.sensors.get(key)) is not None
|
||||||
|
and sensor.error is False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPRECATED_BINARY_SENSORS: Final = (
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="bluetooth",
|
key="bluetooth",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
translation_key="bluetooth",
|
translation_key="bluetooth",
|
||||||
is_on_fn=lambda device, _: device.bluetooth_state,
|
is_on_fn=lambda device, key: False,
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="babyCryDetectionState",
|
key="babyCryDetectionState",
|
||||||
translation_key="baby_cry_detection",
|
translation_key="baby_cry_detection",
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
is_on_fn=lambda device, key: False,
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="beepingApplianceDetectionState",
|
key="beepingApplianceDetectionState",
|
||||||
translation_key="beeping_appliance_detection",
|
translation_key="beeping_appliance_detection",
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
is_on_fn=lambda device, key: False,
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="coughDetectionState",
|
key="coughDetectionState",
|
||||||
translation_key="cough_detection",
|
translation_key="cough_detection",
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
is_on_fn=lambda device, key: False,
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="dogBarkDetectionState",
|
key="dogBarkDetectionState",
|
||||||
translation_key="dog_bark_detection",
|
translation_key="dog_bark_detection",
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
is_on_fn=lambda device, key: False,
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
|
||||||
AmazonBinarySensorEntityDescription(
|
|
||||||
key="humanPresenceDetectionState",
|
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="waterSoundsDetectionState",
|
key="waterSoundsDetectionState",
|
||||||
translation_key="water_sounds_detection",
|
translation_key="water_sounds_detection",
|
||||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
is_on_fn=lambda device, key: False,
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,13 +104,46 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
entity_registry = er.async_get(hass)
|
||||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
|
||||||
for sensor_desc in BINARY_SENSORS
|
# Replace unique id for "detectionState" binary sensor
|
||||||
for serial_num in coordinator.data
|
await async_update_unique_id(
|
||||||
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
hass,
|
||||||
|
coordinator,
|
||||||
|
BINARY_SENSOR_DOMAIN,
|
||||||
|
"humanPresenceDetectionState",
|
||||||
|
"detectionState",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Clean up deprecated sensors
|
||||||
|
for sensor_desc in DEPRECATED_BINARY_SENSORS:
|
||||||
|
for serial_num in coordinator.data:
|
||||||
|
unique_id = f"{serial_num}-{sensor_desc.key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
|
||||||
|
):
|
||||||
|
_LOGGER.debug("Removing deprecated entity %s", entity_id)
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
|
||||||
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
|
def _check_device() -> None:
|
||||||
|
current_devices = set(coordinator.data)
|
||||||
|
new_devices = current_devices - known_devices
|
||||||
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||||
|
for sensor_desc in BINARY_SENSORS
|
||||||
|
for serial_num in new_devices
|
||||||
|
if sensor_desc.is_supported(
|
||||||
|
coordinator.data[serial_num], sensor_desc.key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||||
"""Binary sensor device."""
|
"""Binary sensor device."""
|
||||||
@@ -113,3 +156,13 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
|||||||
return self.entity_description.is_on_fn(
|
return self.entity_description.is_on_fn(
|
||||||
self.device, self.entity_description.key
|
self.device, self.entity_description.key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return (
|
||||||
|
self.entity_description.is_available_fn(
|
||||||
|
self.device, self.entity_description.key
|
||||||
|
)
|
||||||
|
and super().available
|
||||||
|
)
|
||||||
|
@@ -52,7 +52,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Alexa Devices."""
|
"""Handle a config flow for Alexa Devices."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 3
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@@ -64,7 +64,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data = await validate_input(self.hass, user_input)
|
data = await validate_input(self.hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (CannotAuthenticate, TypeError):
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
errors["base"] = "cannot_retrieve_data"
|
||||||
@@ -107,10 +107,12 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
data = await validate_input(
|
||||||
|
self.hass, {**reauth_entry.data, **user_input}
|
||||||
|
)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (CannotAuthenticate, TypeError):
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
errors["base"] = "cannot_retrieve_data"
|
||||||
@@ -119,8 +121,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
reauth_entry,
|
reauth_entry,
|
||||||
data={
|
data={
|
||||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
CONF_CODE: user_input[CONF_CODE],
|
CONF_CODE: user_input[CONF_CODE],
|
||||||
|
CONF_LOGIN_DATA: data,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ _LOGGER = logging.getLogger(__package__)
|
|||||||
|
|
||||||
DOMAIN = "alexa_devices"
|
DOMAIN = "alexa_devices"
|
||||||
CONF_LOGIN_DATA = "login_data"
|
CONF_LOGIN_DATA = "login_data"
|
||||||
|
CONF_SITE = "site"
|
||||||
|
|
||||||
DEFAULT_DOMAIN = "com"
|
DEFAULT_DOMAIN = "com"
|
||||||
COUNTRY_DOMAINS = {
|
COUNTRY_DOMAINS = {
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||||
@@ -48,12 +49,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
entry.data[CONF_LOGIN_DATA],
|
entry.data[CONF_LOGIN_DATA],
|
||||||
)
|
)
|
||||||
|
self.previous_devices: set[str] = set()
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||||
"""Update device data."""
|
"""Update device data."""
|
||||||
try:
|
try:
|
||||||
await self.api.login_mode_stored_data()
|
await self.api.login_mode_stored_data()
|
||||||
return await self.api.get_devices_data()
|
data = await self.api.get_devices_data()
|
||||||
except CannotConnect as err:
|
except CannotConnect as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
@@ -66,9 +68,37 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
translation_key="cannot_retrieve_data_with_error",
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
except (CannotAuthenticate, TypeError) as err:
|
except CannotAuthenticate as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
|
else:
|
||||||
|
current_devices = set(data.keys())
|
||||||
|
if stale_devices := self.previous_devices - current_devices:
|
||||||
|
await self._async_remove_device_stale(stale_devices)
|
||||||
|
|
||||||
|
self.previous_devices = current_devices
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _async_remove_device_stale(
|
||||||
|
self,
|
||||||
|
stale_devices: set[str],
|
||||||
|
) -> None:
|
||||||
|
"""Remove stale device."""
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
|
||||||
|
for serial_num in stale_devices:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Detected change in devices: serial %s removed",
|
||||||
|
serial_num,
|
||||||
|
)
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, serial_num)}
|
||||||
|
)
|
||||||
|
if device:
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_id=device.id,
|
||||||
|
remove_config_entry_id=self.config_entry.entry_id,
|
||||||
|
)
|
||||||
|
@@ -60,7 +60,5 @@ def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
|||||||
"online": device.online,
|
"online": device.online,
|
||||||
"serial number": device.serial_number,
|
"serial number": device.serial_number,
|
||||||
"software version": device.software_version,
|
"software version": device.software_version,
|
||||||
"do not disturb": device.do_not_disturb,
|
"sensors": device.sensors,
|
||||||
"response style": device.response_style,
|
|
||||||
"bluetooth state": device.bluetooth_state,
|
|
||||||
}
|
}
|
||||||
|
@@ -1,44 +1,4 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
|
||||||
"binary_sensor": {
|
|
||||||
"bluetooth": {
|
|
||||||
"default": "mdi:bluetooth-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:bluetooth"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"baby_cry_detection": {
|
|
||||||
"default": "mdi:account-voice-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:account-voice"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"beeping_appliance_detection": {
|
|
||||||
"default": "mdi:bell-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:bell-ring"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cough_detection": {
|
|
||||||
"default": "mdi:blur-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:blur"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dog_bark_detection": {
|
|
||||||
"default": "mdi:dog-side-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:dog-side"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"water_sounds_detection": {
|
|
||||||
"default": "mdi:water-pump-off",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:water-pump"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"send_sound": {
|
"send_sound": {
|
||||||
"service": "mdi:cast-audio"
|
"service": "mdi:cast-audio"
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==6.0.0"]
|
"requirements": ["aioamazondevices==6.2.9"]
|
||||||
}
|
}
|
||||||
|
@@ -57,13 +57,23 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
known_devices: set[str] = set()
|
||||||
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
|
||||||
for sensor_desc in NOTIFY
|
def _check_device() -> None:
|
||||||
for serial_num in coordinator.data
|
current_devices = set(coordinator.data)
|
||||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
new_devices = current_devices - known_devices
|
||||||
and sensor_desc.is_supported(coordinator.data[serial_num])
|
if new_devices:
|
||||||
)
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
||||||
|
for sensor_desc in NOTIFY
|
||||||
|
for serial_num in new_devices
|
||||||
|
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||||
|
and sensor_desc.is_supported(coordinator.data[serial_num])
|
||||||
|
)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||||
|
@@ -53,7 +53,7 @@ rules:
|
|||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: done
|
||||||
dynamic-devices: todo
|
dynamic-devices: done
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
@@ -64,9 +64,7 @@ rules:
|
|||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: no known use cases for repair issues or flows, yet
|
comment: no known use cases for repair issues or flows, yet
|
||||||
stale-devices:
|
stale-devices: done
|
||||||
status: todo
|
|
||||||
comment: automate the cleanup process
|
|
||||||
|
|
||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
|
@@ -31,15 +31,20 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
|||||||
"""Amazon Devices sensor entity description."""
|
"""Amazon Devices sensor entity description."""
|
||||||
|
|
||||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||||
|
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||||
|
device.online
|
||||||
|
and (sensor := device.sensors.get(key)) is not None
|
||||||
|
and sensor.error is False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
SENSORS: Final = (
|
SENSORS: Final = (
|
||||||
AmazonSensorEntityDescription(
|
AmazonSensorEntityDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement_fn=lambda device, _key: (
|
native_unit_of_measurement_fn=lambda device, key: (
|
||||||
UnitOfTemperature.CELSIUS
|
UnitOfTemperature.CELSIUS
|
||||||
if device.sensors[_key].scale == "CELSIUS"
|
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
|
||||||
else UnitOfTemperature.FAHRENHEIT
|
else UnitOfTemperature.FAHRENHEIT
|
||||||
),
|
),
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
@@ -62,12 +67,22 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
known_devices: set[str] = set()
|
||||||
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
|
||||||
for sensor_desc in SENSORS
|
def _check_device() -> None:
|
||||||
for serial_num in coordinator.data
|
current_devices = set(coordinator.data)
|
||||||
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
new_devices = current_devices - known_devices
|
||||||
)
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||||
|
for sensor_desc in SENSORS
|
||||||
|
for serial_num in new_devices
|
||||||
|
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||||
@@ -89,3 +104,13 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
|||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.device.sensors[self.entity_description.key].value
|
return self.device.sensors[self.entity_description.key].value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return (
|
||||||
|
self.entity_description.is_available_fn(
|
||||||
|
self.device, self.entity_description.key
|
||||||
|
)
|
||||||
|
and super().available
|
||||||
|
)
|
||||||
|
@@ -58,26 +58,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
|
||||||
"bluetooth": {
|
|
||||||
"name": "Bluetooth"
|
|
||||||
},
|
|
||||||
"baby_cry_detection": {
|
|
||||||
"name": "Baby crying"
|
|
||||||
},
|
|
||||||
"beeping_appliance_detection": {
|
|
||||||
"name": "Beeping appliance"
|
|
||||||
},
|
|
||||||
"cough_detection": {
|
|
||||||
"name": "Coughing"
|
|
||||||
},
|
|
||||||
"dog_bark_detection": {
|
|
||||||
"name": "Dog barking"
|
|
||||||
},
|
|
||||||
"water_sounds_detection": {
|
|
||||||
"name": "Water sounds"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notify": {
|
"notify": {
|
||||||
"speak": {
|
"speak": {
|
||||||
"name": "Speak"
|
"name": "Speak"
|
||||||
@@ -104,10 +84,6 @@
|
|||||||
"sound": {
|
"sound": {
|
||||||
"name": "Alexa Skill sound file",
|
"name": "Alexa Skill sound file",
|
||||||
"description": "The sound file to play."
|
"description": "The sound file to play."
|
||||||
},
|
|
||||||
"sound_variant": {
|
|
||||||
"name": "Sound variant",
|
|
||||||
"description": "The variant of the sound to play."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -8,13 +8,21 @@ from typing import TYPE_CHECKING, Any, Final
|
|||||||
|
|
||||||
from aioamazondevices.api import AmazonDevice
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import (
|
||||||
|
DOMAIN as SWITCH_DOMAIN,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
from .utils import alexa_api_call
|
from .utils import (
|
||||||
|
alexa_api_call,
|
||||||
|
async_remove_dnd_from_virtual_group,
|
||||||
|
async_update_unique_id,
|
||||||
|
)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -24,16 +32,19 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
|||||||
"""Alexa Devices switch entity description."""
|
"""Alexa Devices switch entity description."""
|
||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice], bool]
|
||||||
subkey: str
|
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||||
|
device.online
|
||||||
|
and (sensor := device.sensors.get(key)) is not None
|
||||||
|
and sensor.error is False
|
||||||
|
)
|
||||||
method: str
|
method: str
|
||||||
|
|
||||||
|
|
||||||
SWITCHES: Final = (
|
SWITCHES: Final = (
|
||||||
AmazonSwitchEntityDescription(
|
AmazonSwitchEntityDescription(
|
||||||
key="do_not_disturb",
|
key="dnd",
|
||||||
subkey="AUDIO_PLAYER",
|
|
||||||
translation_key="do_not_disturb",
|
translation_key="do_not_disturb",
|
||||||
is_on_fn=lambda _device: _device.do_not_disturb,
|
is_on_fn=lambda device: bool(device.sensors["dnd"].value),
|
||||||
method="set_do_not_disturb",
|
method="set_do_not_disturb",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -48,13 +59,31 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
# Replace unique id for "DND" switch and remove from Speaker Group
|
||||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
await async_update_unique_id(
|
||||||
for switch_desc in SWITCHES
|
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
||||||
for serial_num in coordinator.data
|
|
||||||
if switch_desc.subkey in coordinator.data[serial_num].capabilities
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Remove DND switch from virtual groups
|
||||||
|
await async_remove_dnd_from_virtual_group(hass, coordinator)
|
||||||
|
|
||||||
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
|
def _check_device() -> None:
|
||||||
|
current_devices = set(coordinator.data)
|
||||||
|
new_devices = current_devices - known_devices
|
||||||
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||||
|
for switch_desc in SWITCHES
|
||||||
|
for serial_num in new_devices
|
||||||
|
if switch_desc.key in coordinator.data[serial_num].sensors
|
||||||
|
)
|
||||||
|
|
||||||
|
_check_device()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||||
|
|
||||||
|
|
||||||
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||||
"""Switch device."""
|
"""Switch device."""
|
||||||
@@ -84,3 +113,13 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
|||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if switch is on."""
|
"""Return True if switch is on."""
|
||||||
return self.entity_description.is_on_fn(self.device)
|
return self.entity_description.is_on_fn(self.device)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return (
|
||||||
|
self.entity_description.is_available_fn(
|
||||||
|
self.device, self.entity_description.key
|
||||||
|
)
|
||||||
|
and super().available
|
||||||
|
)
|
||||||
|
@@ -4,11 +4,16 @@ from collections.abc import Awaitable, Callable, Coroutine
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Concatenate
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
|
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import _LOGGER, DOMAIN
|
||||||
|
from .coordinator import AmazonDevicesCoordinator
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -38,3 +43,41 @@ def alexa_api_call[_T: AmazonEntity, **_P](
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
return cmd_wrapper
|
return cmd_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
async def async_update_unique_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
coordinator: AmazonDevicesCoordinator,
|
||||||
|
domain: str,
|
||||||
|
old_key: str,
|
||||||
|
new_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Update unique id for entities created with old format."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
for serial_num in coordinator.data:
|
||||||
|
unique_id = f"{serial_num}-{old_key}"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
|
||||||
|
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
||||||
|
new_unique_id = unique_id.replace(old_key, new_key)
|
||||||
|
|
||||||
|
# Update the registry with the new unique_id
|
||||||
|
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_dnd_from_virtual_group(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
coordinator: AmazonDevicesCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Remove entity DND from virtual group."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
for serial_num in coordinator.data:
|
||||||
|
unique_id = f"{serial_num}-do_not_disturb"
|
||||||
|
entity_id = entity_registry.async_get_entity_id(
|
||||||
|
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||||
|
)
|
||||||
|
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||||
|
if entity_id and is_group:
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||||
|
@@ -65,6 +65,31 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
translation_placeholders={"sensor_name": "BME280"},
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
),
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="BME680_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME680"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BME680_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BME680"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BME680_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME680"},
|
||||||
|
),
|
||||||
AltruistSensorEntityDescription(
|
AltruistSensorEntityDescription(
|
||||||
device_class=SensorDeviceClass.PRESSURE,
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
key="BMP_pressure",
|
key="BMP_pressure",
|
||||||
|
@@ -41,7 +41,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
call_ids = await async_extract_entity_ids(hass, call)
|
call_ids = await async_extract_entity_ids(call)
|
||||||
entity_ids = []
|
entity_ids = []
|
||||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||||
if entity_id not in call_ids:
|
if entity_id not in call_ids:
|
||||||
|
@@ -12,10 +12,25 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .analytics import Analytics
|
from .analytics import (
|
||||||
|
Analytics,
|
||||||
|
AnalyticsInput,
|
||||||
|
AnalyticsModifications,
|
||||||
|
DeviceAnalyticsModifications,
|
||||||
|
EntityAnalyticsModifications,
|
||||||
|
async_devices_payload,
|
||||||
|
)
|
||||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||||
from .http import AnalyticsDevicesView
|
from .http import AnalyticsDevicesView
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AnalyticsInput",
|
||||||
|
"AnalyticsModifications",
|
||||||
|
"DeviceAnalyticsModifications",
|
||||||
|
"EntityAnalyticsModifications",
|
||||||
|
"async_devices_payload",
|
||||||
|
]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||||
|
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
from dataclasses import asdict as dataclass_asdict, dataclass
|
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||||||
|
from dataclasses import asdict as dataclass_asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any, Protocol
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -24,17 +25,25 @@ from homeassistant.components.recorder import (
|
|||||||
get_instance as get_recorder_instance,
|
get_instance as get_recorder_instance,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IGNORE
|
from homeassistant.config_entries import SOURCE_IGNORE
|
||||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
from homeassistant.const import (
|
||||||
|
ATTR_ASSUMED_STATE,
|
||||||
|
ATTR_DOMAIN,
|
||||||
|
BASE_PLATFORMS,
|
||||||
|
__version__ as HA_VERSION,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.system_info import async_get_system_info
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
|
from homeassistant.helpers.typing import UNDEFINED
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationNotFound,
|
IntegrationNotFound,
|
||||||
|
async_get_integration,
|
||||||
async_get_integrations,
|
async_get_integrations,
|
||||||
)
|
)
|
||||||
from homeassistant.setup import async_get_loaded_integrations
|
from homeassistant.setup import async_get_loaded_integrations
|
||||||
@@ -70,12 +79,115 @@ from .const import (
|
|||||||
ATTR_USER_COUNT,
|
ATTR_USER_COUNT,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
PREFERENCE_SCHEMA,
|
PREFERENCE_SCHEMA,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DATA_ANALYTICS_MODIFIERS = "analytics_modifiers"
|
||||||
|
|
||||||
|
type AnalyticsModifier = Callable[
|
||||||
|
[HomeAssistant, AnalyticsInput], Awaitable[AnalyticsModifications]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@singleton(DATA_ANALYTICS_MODIFIERS)
|
||||||
|
def _async_get_modifiers(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> dict[str, AnalyticsModifier | None]:
|
||||||
|
"""Return the analytics modifiers."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AnalyticsInput:
|
||||||
|
"""Analytics input for a single integration.
|
||||||
|
|
||||||
|
This is sent to integrations that implement the platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
device_ids: Iterable[str] = field(default_factory=list)
|
||||||
|
entity_ids: Iterable[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AnalyticsModifications:
|
||||||
|
"""Analytics config for a single integration.
|
||||||
|
|
||||||
|
This is used by integrations that implement the platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
remove: bool = False
|
||||||
|
devices: Mapping[str, DeviceAnalyticsModifications] | None = None
|
||||||
|
entities: Mapping[str, EntityAnalyticsModifications] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceAnalyticsModifications:
|
||||||
|
"""Analytics config for a single device.
|
||||||
|
|
||||||
|
This is used by integrations that implement the platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
remove: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EntityAnalyticsModifications:
|
||||||
|
"""Analytics config for a single entity.
|
||||||
|
|
||||||
|
This is used by integrations that implement the platform.
|
||||||
|
"""
|
||||||
|
|
||||||
|
remove: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsPlatformProtocol(Protocol):
|
||||||
|
"""Define the format of analytics platforms."""
|
||||||
|
|
||||||
|
async def async_modify_analytics(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
analytics_input: AnalyticsInput,
|
||||||
|
) -> AnalyticsModifications:
|
||||||
|
"""Modify the analytics."""
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_analytics_platform(
|
||||||
|
hass: HomeAssistant, domain: str
|
||||||
|
) -> AnalyticsPlatformProtocol | None:
|
||||||
|
"""Get analytics platform."""
|
||||||
|
try:
|
||||||
|
integration = await async_get_integration(hass, domain)
|
||||||
|
except IntegrationNotFound:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return await integration.async_get_platform(DOMAIN)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_get_modifier(
|
||||||
|
hass: HomeAssistant, domain: str
|
||||||
|
) -> AnalyticsModifier | None:
|
||||||
|
"""Get analytics modifier."""
|
||||||
|
modifiers = _async_get_modifiers(hass)
|
||||||
|
modifier = modifiers.get(domain, UNDEFINED)
|
||||||
|
|
||||||
|
if modifier is not UNDEFINED:
|
||||||
|
return modifier
|
||||||
|
|
||||||
|
platform = await _async_get_analytics_platform(hass, domain)
|
||||||
|
if platform is None:
|
||||||
|
modifiers[domain] = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
modifier = getattr(platform, "async_modify_analytics", None)
|
||||||
|
modifiers[domain] = modifier
|
||||||
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
def gen_uuid() -> str:
|
def gen_uuid() -> str:
|
||||||
"""Generate a new UUID."""
|
"""Generate a new UUID."""
|
||||||
@@ -388,67 +500,219 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
|||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
DEFAULT_ANALYTICS_CONFIG = AnalyticsModifications()
|
||||||
"""Return the devices payload."""
|
DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
|
||||||
devices: list[dict[str, Any]] = []
|
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
|
||||||
|
"""Return detailed information about entities and devices."""
|
||||||
dev_reg = dr.async_get(hass)
|
dev_reg = dr.async_get(hass)
|
||||||
# Devices that need via device info set
|
ent_reg = er.async_get(hass)
|
||||||
new_indexes: dict[str, int] = {}
|
|
||||||
via_devices: dict[str, str] = {}
|
|
||||||
|
|
||||||
seen_integrations = set()
|
integration_inputs: dict[str, tuple[list[str], list[str]]] = {}
|
||||||
|
integration_configs: dict[str, AnalyticsModifications] = {}
|
||||||
|
|
||||||
for device in dev_reg.devices.values():
|
removed_devices: set[str] = set()
|
||||||
if not device.primary_config_entry:
|
|
||||||
|
# Get device list
|
||||||
|
for device_entry in dev_reg.devices.values():
|
||||||
|
if not device_entry.primary_config_entry:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config_entry = hass.config_entries.async_get_entry(device.primary_config_entry)
|
config_entry = hass.config_entries.async_get_entry(
|
||||||
|
device_entry.primary_config_entry
|
||||||
|
)
|
||||||
|
|
||||||
if config_entry is None:
|
if config_entry is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
seen_integrations.add(config_entry.domain)
|
if device_entry.entry_type is dr.DeviceEntryType.SERVICE:
|
||||||
|
removed_devices.add(device_entry.id)
|
||||||
new_indexes[device.id] = len(devices)
|
|
||||||
devices.append(
|
|
||||||
{
|
|
||||||
"integration": config_entry.domain,
|
|
||||||
"manufacturer": device.manufacturer,
|
|
||||||
"model_id": device.model_id,
|
|
||||||
"model": device.model,
|
|
||||||
"sw_version": device.sw_version,
|
|
||||||
"hw_version": device.hw_version,
|
|
||||||
"has_configuration_url": device.configuration_url is not None,
|
|
||||||
"via_device": None,
|
|
||||||
"entry_type": device.entry_type.value if device.entry_type else None,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if device.via_device_id:
|
|
||||||
via_devices[device.id] = device.via_device_id
|
|
||||||
|
|
||||||
for from_device, via_device in via_devices.items():
|
|
||||||
if via_device not in new_indexes:
|
|
||||||
continue
|
continue
|
||||||
devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device]
|
|
||||||
|
integration_domain = config_entry.domain
|
||||||
|
|
||||||
|
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
|
||||||
|
integration_input[0].append(device_entry.id)
|
||||||
|
|
||||||
|
# Get entity list
|
||||||
|
for entity_entry in ent_reg.entities.values():
|
||||||
|
integration_domain = entity_entry.platform
|
||||||
|
|
||||||
|
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
|
||||||
|
integration_input[1].append(entity_entry.entity_id)
|
||||||
|
|
||||||
integrations = {
|
integrations = {
|
||||||
domain: integration
|
domain: integration
|
||||||
for domain, integration in (
|
for domain, integration in (
|
||||||
await async_get_integrations(hass, seen_integrations)
|
await async_get_integrations(hass, integration_inputs.keys())
|
||||||
).items()
|
).items()
|
||||||
if isinstance(integration, Integration)
|
if isinstance(integration, Integration)
|
||||||
}
|
}
|
||||||
|
|
||||||
for device_info in devices:
|
# Filter out custom integrations and integrations that are not device or hub type
|
||||||
if integration := integrations.get(device_info["integration"]):
|
integration_inputs = {
|
||||||
device_info["is_custom_integration"] = not integration.is_built_in
|
domain: integration_info
|
||||||
# Include version for custom integrations
|
for domain, integration_info in integration_inputs.items()
|
||||||
if not integration.is_built_in and integration.version:
|
if (integration := integrations.get(domain)) is not None
|
||||||
device_info["custom_integration_version"] = str(integration.version)
|
and integration.is_built_in
|
||||||
|
and integration.manifest.get("integration_type") in ("device", "hub")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call integrations that implement the analytics platform
|
||||||
|
for integration_domain, integration_input in integration_inputs.items():
|
||||||
|
if (
|
||||||
|
modifier := await _async_get_modifier(hass, integration_domain)
|
||||||
|
) is not None:
|
||||||
|
try:
|
||||||
|
integration_config = await modifier(
|
||||||
|
hass, AnalyticsInput(*integration_input)
|
||||||
|
)
|
||||||
|
except Exception as err: # noqa: BLE001
|
||||||
|
LOGGER.exception(
|
||||||
|
"Calling async_modify_analytics for integration '%s' failed: %s",
|
||||||
|
integration_domain,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
integration_configs[integration_domain] = AnalyticsModifications(
|
||||||
|
remove=True
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(integration_config, AnalyticsModifications):
|
||||||
|
LOGGER.error( # type: ignore[unreachable]
|
||||||
|
"Calling async_modify_analytics for integration '%s' did not return an AnalyticsConfig",
|
||||||
|
integration_domain,
|
||||||
|
)
|
||||||
|
integration_configs[integration_domain] = AnalyticsModifications(
|
||||||
|
remove=True
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
integration_configs[integration_domain] = integration_config
|
||||||
|
|
||||||
|
integrations_info: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
# We need to refer to other devices, for example in `via_device` field.
|
||||||
|
# We don't however send the original device ids outside of Home Assistant,
|
||||||
|
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
|
||||||
|
device_id_mapping: dict[str, tuple[str, int]] = {}
|
||||||
|
|
||||||
|
# Fill out information about devices
|
||||||
|
for integration_domain, integration_input in integration_inputs.items():
|
||||||
|
integration_config = integration_configs.get(
|
||||||
|
integration_domain, DEFAULT_ANALYTICS_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
if integration_config.remove:
|
||||||
|
continue
|
||||||
|
|
||||||
|
integration_info = integrations_info.setdefault(
|
||||||
|
integration_domain, {"devices": [], "entities": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
devices_info = integration_info["devices"]
|
||||||
|
|
||||||
|
for device_id in integration_input[0]:
|
||||||
|
device_config = DEFAULT_DEVICE_ANALYTICS_CONFIG
|
||||||
|
if integration_config.devices is not None:
|
||||||
|
device_config = integration_config.devices.get(device_id, device_config)
|
||||||
|
|
||||||
|
if device_config.remove:
|
||||||
|
removed_devices.add(device_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
device_entry = dev_reg.devices[device_id]
|
||||||
|
|
||||||
|
device_id_mapping[device_id] = (integration_domain, len(devices_info))
|
||||||
|
|
||||||
|
devices_info.append(
|
||||||
|
{
|
||||||
|
"entry_type": device_entry.entry_type,
|
||||||
|
"has_configuration_url": device_entry.configuration_url is not None,
|
||||||
|
"hw_version": device_entry.hw_version,
|
||||||
|
"manufacturer": device_entry.manufacturer,
|
||||||
|
"model": device_entry.model,
|
||||||
|
"model_id": device_entry.model_id,
|
||||||
|
"sw_version": device_entry.sw_version,
|
||||||
|
"via_device": device_entry.via_device_id,
|
||||||
|
"entities": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fill out via_device with new device ids
|
||||||
|
for integration_info in integrations_info.values():
|
||||||
|
for device_info in integration_info["devices"]:
|
||||||
|
if device_info["via_device"] is None:
|
||||||
|
continue
|
||||||
|
device_info["via_device"] = device_id_mapping.get(device_info["via_device"])
|
||||||
|
|
||||||
|
# Fill out information about entities
|
||||||
|
for integration_domain, integration_input in integration_inputs.items():
|
||||||
|
integration_config = integration_configs.get(
|
||||||
|
integration_domain, DEFAULT_ANALYTICS_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
if integration_config.remove:
|
||||||
|
continue
|
||||||
|
|
||||||
|
integration_info = integrations_info.setdefault(
|
||||||
|
integration_domain, {"devices": [], "entities": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
devices_info = integration_info["devices"]
|
||||||
|
entities_info = integration_info["entities"]
|
||||||
|
|
||||||
|
for entity_id in integration_input[1]:
|
||||||
|
entity_config = DEFAULT_ENTITY_ANALYTICS_CONFIG
|
||||||
|
if integration_config.entities is not None:
|
||||||
|
entity_config = integration_config.entities.get(
|
||||||
|
entity_id, entity_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity_config.remove:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entity_entry = ent_reg.entities[entity_id]
|
||||||
|
|
||||||
|
entity_state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
entity_info = {
|
||||||
|
# LIMITATION: `assumed_state` can be overridden by users;
|
||||||
|
# we should replace it with the original value in the future.
|
||||||
|
# It is also not present, if entity is not in the state machine,
|
||||||
|
# which can happen for disabled entities.
|
||||||
|
"assumed_state": (
|
||||||
|
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
||||||
|
if entity_state is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"domain": entity_entry.domain,
|
||||||
|
"entity_category": entity_entry.entity_category,
|
||||||
|
"has_entity_name": entity_entry.has_entity_name,
|
||||||
|
"original_device_class": entity_entry.original_device_class,
|
||||||
|
# LIMITATION: `unit_of_measurement` can be overridden by users;
|
||||||
|
# we should replace it with the original value in the future.
|
||||||
|
"unit_of_measurement": entity_entry.unit_of_measurement,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device_id_ := entity_entry.device_id) is not None:
|
||||||
|
if device_id_ in removed_devices:
|
||||||
|
# The device was removed, so we remove the entity too
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
new_device_id := device_id_mapping.get(device_id_)
|
||||||
|
) is not None and (new_device_id[0] == integration_domain):
|
||||||
|
device_info = devices_info[new_device_id[1]]
|
||||||
|
device_info["entities"].append(entity_info)
|
||||||
|
continue
|
||||||
|
|
||||||
|
entities_info.append(entity_info)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": "home-assistant:1",
|
"version": "home-assistant:1",
|
||||||
"home_assistant": HA_VERSION,
|
"home_assistant": HA_VERSION,
|
||||||
"devices": devices,
|
"integrations": integrations_info,
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "analytics",
|
"domain": "analytics",
|
||||||
"name": "Analytics",
|
"name": "Analytics",
|
||||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||||
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["api", "websocket_api", "http"],
|
"dependencies": ["api", "websocket_api", "http"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
|
@@ -33,9 +33,11 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.storage import STORAGE_DIR
|
from homeassistant.helpers.storage import STORAGE_DIR
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ADB_SERVER_IP,
|
CONF_ADB_SERVER_IP,
|
||||||
@@ -46,10 +48,12 @@ from .const import (
|
|||||||
DEFAULT_ADB_SERVER_PORT,
|
DEFAULT_ADB_SERVER_PORT,
|
||||||
DEVICE_ANDROIDTV,
|
DEVICE_ANDROIDTV,
|
||||||
DEVICE_FIRETV,
|
DEVICE_FIRETV,
|
||||||
|
DOMAIN,
|
||||||
PROP_ETHMAC,
|
PROP_ETHMAC,
|
||||||
PROP_WIFIMAC,
|
PROP_WIFIMAC,
|
||||||
SIGNAL_CONFIG_ENTITY,
|
SIGNAL_CONFIG_ENTITY,
|
||||||
)
|
)
|
||||||
|
from .services import async_setup_services
|
||||||
|
|
||||||
ADB_PYTHON_EXCEPTIONS: tuple = (
|
ADB_PYTHON_EXCEPTIONS: tuple = (
|
||||||
AdbTimeoutError,
|
AdbTimeoutError,
|
||||||
@@ -63,6 +67,8 @@ ADB_PYTHON_EXCEPTIONS: tuple = (
|
|||||||
)
|
)
|
||||||
ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
|
ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
|
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
|
||||||
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
||||||
|
|
||||||
@@ -188,6 +194,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Android TV / Fire TV integration."""
|
||||||
|
async_setup_services(hass)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
||||||
"""Set up Android Debug Bridge platform."""
|
"""Set up Android Debug Bridge platform."""
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ import logging
|
|||||||
|
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
@@ -17,9 +16,7 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_COMMAND
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
@@ -39,19 +36,10 @@ from .const import (
|
|||||||
SIGNAL_CONFIG_ENTITY,
|
SIGNAL_CONFIG_ENTITY,
|
||||||
)
|
)
|
||||||
from .entity import AndroidTVEntity, adb_decorator
|
from .entity import AndroidTVEntity, adb_decorator
|
||||||
|
from .services import ATTR_ADB_RESPONSE, ATTR_HDMI_INPUT, SERVICE_LEARN_SENDEVENT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_ADB_RESPONSE = "adb_response"
|
|
||||||
ATTR_DEVICE_PATH = "device_path"
|
|
||||||
ATTR_HDMI_INPUT = "hdmi_input"
|
|
||||||
ATTR_LOCAL_PATH = "local_path"
|
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND = "adb_command"
|
|
||||||
SERVICE_DOWNLOAD = "download"
|
|
||||||
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
|
||||||
SERVICE_UPLOAD = "upload"
|
|
||||||
|
|
||||||
# Translate from `AndroidTV` / `FireTV` reported state to HA state.
|
# Translate from `AndroidTV` / `FireTV` reported state to HA state.
|
||||||
ANDROIDTV_STATES = {
|
ANDROIDTV_STATES = {
|
||||||
"off": MediaPlayerState.OFF,
|
"off": MediaPlayerState.OFF,
|
||||||
@@ -77,32 +65,6 @@ async def async_setup_entry(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
platform = entity_platform.async_get_current_platform()
|
|
||||||
platform.async_register_entity_service(
|
|
||||||
SERVICE_ADB_COMMAND,
|
|
||||||
{vol.Required(ATTR_COMMAND): cv.string},
|
|
||||||
"adb_command",
|
|
||||||
)
|
|
||||||
platform.async_register_entity_service(
|
|
||||||
SERVICE_LEARN_SENDEVENT, None, "learn_sendevent"
|
|
||||||
)
|
|
||||||
platform.async_register_entity_service(
|
|
||||||
SERVICE_DOWNLOAD,
|
|
||||||
{
|
|
||||||
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
|
||||||
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
|
||||||
},
|
|
||||||
"service_download",
|
|
||||||
)
|
|
||||||
platform.async_register_entity_service(
|
|
||||||
SERVICE_UPLOAD,
|
|
||||||
{
|
|
||||||
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
|
||||||
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
|
||||||
},
|
|
||||||
"service_upload",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||||
"""Representation of an Android or Fire TV device."""
|
"""Representation of an Android or Fire TV device."""
|
||||||
|
66
homeassistant/components/androidtv/services.py
Normal file
66
homeassistant/components/androidtv/services.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""Services for Android/Fire TV devices."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||||
|
from homeassistant.const import ATTR_COMMAND
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import config_validation as cv, service
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
ATTR_ADB_RESPONSE = "adb_response"
|
||||||
|
ATTR_DEVICE_PATH = "device_path"
|
||||||
|
ATTR_HDMI_INPUT = "hdmi_input"
|
||||||
|
ATTR_LOCAL_PATH = "local_path"
|
||||||
|
|
||||||
|
SERVICE_ADB_COMMAND = "adb_command"
|
||||||
|
SERVICE_DOWNLOAD = "download"
|
||||||
|
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||||
|
SERVICE_UPLOAD = "upload"
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Register the Android TV / Fire TV services."""
|
||||||
|
|
||||||
|
service.async_register_platform_entity_service(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADB_COMMAND,
|
||||||
|
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||||
|
schema={vol.Required(ATTR_COMMAND): cv.string},
|
||||||
|
func="adb_command",
|
||||||
|
)
|
||||||
|
service.async_register_platform_entity_service(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_LEARN_SENDEVENT,
|
||||||
|
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||||
|
schema=None,
|
||||||
|
func="learn_sendevent",
|
||||||
|
)
|
||||||
|
service.async_register_platform_entity_service(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_DOWNLOAD,
|
||||||
|
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||||
|
schema={
|
||||||
|
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
||||||
|
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
||||||
|
},
|
||||||
|
func="service_download",
|
||||||
|
)
|
||||||
|
service.async_register_platform_entity_service(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_UPLOAD,
|
||||||
|
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||||
|
schema={
|
||||||
|
vol.Required(ATTR_DEVICE_PATH): cv.string,
|
||||||
|
vol.Required(ATTR_LOCAL_PATH): cv.string,
|
||||||
|
},
|
||||||
|
func="service_upload",
|
||||||
|
)
|
@@ -37,7 +37,7 @@ from .helpers import AndroidTVRemoteConfigEntry, create_api, get_enable_ime
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
APPS_NEW_ID = "NewApp"
|
APPS_NEW_ID = "add_new"
|
||||||
CONF_APP_DELETE = "app_delete"
|
CONF_APP_DELETE = "app_delete"
|
||||||
CONF_APP_ID = "app_id"
|
CONF_APP_ID = "app_id"
|
||||||
|
|
||||||
@@ -66,9 +66,14 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.host = user_input[CONF_HOST]
|
self.host = user_input[CONF_HOST]
|
||||||
api = create_api(self.hass, self.host, enable_ime=False)
|
api = create_api(self.hass, self.host, enable_ime=False)
|
||||||
|
await api.async_generate_cert_if_missing()
|
||||||
try:
|
try:
|
||||||
await api.async_generate_cert_if_missing()
|
|
||||||
self.name, self.mac = await api.async_get_name_and_mac()
|
self.name, self.mac = await api.async_get_name_and_mac()
|
||||||
|
except CannotConnect:
|
||||||
|
# Likely invalid IP address or device is network unreachable. Stay
|
||||||
|
# in the user step allowing the user to enter a different host.
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
await self.async_set_unique_id(format_mac(self.mac))
|
await self.async_set_unique_id(format_mac(self.mac))
|
||||||
if self.source == SOURCE_RECONFIGURE:
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
self._abort_if_unique_id_mismatch()
|
self._abort_if_unique_id_mismatch()
|
||||||
@@ -81,11 +86,10 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||||
return await self._async_start_pair()
|
try:
|
||||||
except (CannotConnect, ConnectionClosed):
|
return await self._async_start_pair()
|
||||||
# Likely invalid IP address or device is network unreachable. Stay
|
except (CannotConnect, ConnectionClosed):
|
||||||
# in the user step allowing the user to enter a different host.
|
errors["base"] = "cannot_connect"
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
else:
|
else:
|
||||||
user_input = {}
|
user_input = {}
|
||||||
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
||||||
@@ -112,22 +116,9 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the pair step."""
|
"""Handle the pair step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
pin = user_input["pin"]
|
||||||
try:
|
try:
|
||||||
pin = user_input["pin"]
|
|
||||||
await self.api.async_finish_pairing(pin)
|
await self.api.async_finish_pairing(pin)
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self.name,
|
|
||||||
data={
|
|
||||||
CONF_HOST: self.host,
|
|
||||||
CONF_NAME: self.name,
|
|
||||||
CONF_MAC: self.mac,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
# Invalid PIN. Stay in the pair step allowing the user to enter
|
# Invalid PIN. Stay in the pair step allowing the user to enter
|
||||||
# a different PIN.
|
# a different PIN.
|
||||||
@@ -145,6 +136,20 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# them to enter a new IP address but we cannot do that for the zeroconf
|
# them to enter a new IP address but we cannot do that for the zeroconf
|
||||||
# flow. Simpler to abort for both flows.
|
# flow. Simpler to abort for both flows.
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
else:
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.name,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.host,
|
||||||
|
CONF_NAME: self.name,
|
||||||
|
CONF_MAC: self.mac,
|
||||||
|
},
|
||||||
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="pair",
|
step_id="pair",
|
||||||
data_schema=STEP_PAIR_DATA_SCHEMA,
|
data_schema=STEP_PAIR_DATA_SCHEMA,
|
||||||
@@ -282,7 +287,9 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
|
|||||||
{
|
{
|
||||||
vol.Optional(CONF_APPS): SelectSelector(
|
vol.Optional(CONF_APPS): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=apps, mode=SelectSelectorMode.DROPDOWN
|
options=apps,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
translation_key="apps",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
vol.Required(
|
vol.Required(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user