mirror of
https://github.com/home-assistant/core.git
synced 2025-09-23 03:49:31 +00:00
Compare commits
2508 Commits
2025.4.2
...
block_pyse
Author | SHA1 | Date | |
---|---|---|---|
![]() |
162c27b92c | ||
![]() |
3dc7b75e4b | ||
![]() |
c14d17f88c | ||
![]() |
b7ce0f63a9 | ||
![]() |
c2a5e1aaf9 | ||
![]() |
13d7234f97 | ||
![]() |
ca50fca738 | ||
![]() |
acbfe54c7b | ||
![]() |
49f9166646 | ||
![]() |
42cacd28e7 | ||
![]() |
6f9a39ab89 | ||
![]() |
0d81694640 | ||
![]() |
109bcf362a | ||
![]() |
0260a03447 | ||
![]() |
0802fc8a21 | ||
![]() |
c346b932f0 | ||
![]() |
a14f3ab6b1 | ||
![]() |
14cd00a116 | ||
![]() |
486535c189 | ||
![]() |
54dce53628 | ||
![]() |
a3b7cd7b4d | ||
![]() |
dafda420e5 | ||
![]() |
68a4e1a112 | ||
![]() |
d3275c3833 | ||
![]() |
2d5867cab6 | ||
![]() |
49cf66269c | ||
![]() |
5642d6450f | ||
![]() |
6ddc2193d6 | ||
![]() |
5202bbb6af | ||
![]() |
39906cf65b | ||
![]() |
c7745e0d02 | ||
![]() |
1c1f5a779b | ||
![]() |
ba0f6c3ba2 | ||
![]() |
150110e221 | ||
![]() |
e95e9e1a33 | ||
![]() |
c68ab714b7 | ||
![]() |
2d2e0d0fb9 | ||
![]() |
970359c6a0 | ||
![]() |
e22fbe553b | ||
![]() |
87c3e2c7ce | ||
![]() |
13a6c13b89 | ||
![]() |
cc504da03a | ||
![]() |
25f3ab3640 | ||
![]() |
2cf09abb4c | ||
![]() |
c1c74a6f61 | ||
![]() |
8f9f531dd7 | ||
![]() |
34d11521c0 | ||
![]() |
561be22a60 | ||
![]() |
301d308d5a | ||
![]() |
d975135a7c | ||
![]() |
19ee8886d6 | ||
![]() |
7f4cc99a3e | ||
![]() |
d4333665fc | ||
![]() |
ba0c03ddbb | ||
![]() |
32eb4af6ef | ||
![]() |
e4b519d77a | ||
![]() |
14c4cf7b63 | ||
![]() |
1cc2baa95e | ||
![]() |
f472bf7c87 | ||
![]() |
bc9683312e | ||
![]() |
6634efa3aa | ||
![]() |
d0b2331a5f | ||
![]() |
46951bf223 | ||
![]() |
565f051ffc | ||
![]() |
8c971904ca | ||
![]() |
d0bc71752b | ||
![]() |
6b1484a7f0 | ||
![]() |
5eebadc730 | ||
![]() |
fa37bc272e | ||
![]() |
535d128f8a | ||
![]() |
13d530d110 | ||
![]() |
57f754b42b | ||
![]() |
1e0a2b704f | ||
![]() |
526a8ee31f | ||
![]() |
ce02a5544d | ||
![]() |
1044a5341d | ||
![]() |
a707cbc51b | ||
![]() |
adf8e50313 | ||
![]() |
8356bdb506 | ||
![]() |
5c7aa833ec | ||
![]() |
f92d14d87c | ||
![]() |
2d3a6d780c | ||
![]() |
c359765a29 | ||
![]() |
d8ed10bcc7 | ||
![]() |
19259d5cad | ||
![]() |
102230bf9d | ||
![]() |
2a38f03ec9 | ||
![]() |
e22ea85e84 | ||
![]() |
ed0ff93d1e | ||
![]() |
3e6473d130 | ||
![]() |
7af731694f | ||
![]() |
83ec45e4fc | ||
![]() |
086e97821f | ||
![]() |
5048d1512c | ||
![]() |
199c565bf2 | ||
![]() |
cbeefdaf26 | ||
![]() |
4747de4703 | ||
![]() |
9a183bc16a | ||
![]() |
e540247c14 | ||
![]() |
0aef8b58d8 | ||
![]() |
f0501f917b | ||
![]() |
97004e13cb | ||
![]() |
f867a0af24 | ||
![]() |
d3b3839ffa | ||
![]() |
1a227d6a10 | ||
![]() |
fc8c403a3a | ||
![]() |
c1bf596eba | ||
![]() |
63f69a9e3d | ||
![]() |
e13b014b6f | ||
![]() |
be0d4d926c | ||
![]() |
2403fff81f | ||
![]() |
8c475787cc | ||
![]() |
d9fe1edd82 | ||
![]() |
f5cf64700a | ||
![]() |
777b04d7a5 | ||
![]() |
9fc78ed4e2 | ||
![]() |
d03af549d4 | ||
![]() |
d91f01243c | ||
![]() |
5094208db6 | ||
![]() |
006f66a841 | ||
![]() |
64b7d77840 | ||
![]() |
abf6a809b8 | ||
![]() |
1b7dd205c7 | ||
![]() |
3e00366a61 | ||
![]() |
a17275b559 | ||
![]() |
9534a919ce | ||
![]() |
422dbfef88 | ||
![]() |
8e44684a61 | ||
![]() |
642e7fd487 | ||
![]() |
9bb9132e7b | ||
![]() |
41be82f167 | ||
![]() |
47140e14d9 | ||
![]() |
926502b0f1 | ||
![]() |
78351ff7a7 | ||
![]() |
c333726867 | ||
![]() |
fc2fe32f34 | ||
![]() |
528a509479 | ||
![]() |
bca4793c69 | ||
![]() |
0c9b1b5c58 | ||
![]() |
7bf4239789 | ||
![]() |
71ac2d3d75 | ||
![]() |
f019e8a36c | ||
![]() |
44560dd298 | ||
![]() |
e8ea5c9d62 | ||
![]() |
17297ab929 | ||
![]() |
041c09380b | ||
![]() |
553d420db9 | ||
![]() |
3f99a0bb65 | ||
![]() |
c3d318ff51 | ||
![]() |
19345b0e18 | ||
![]() |
e13abf2034 | ||
![]() |
61248c561d | ||
![]() |
8561721faf | ||
![]() |
2f318927bc | ||
![]() |
a15572bb8c | ||
![]() |
b532776d78 | ||
![]() |
4ad34c57b5 | ||
![]() |
228beacca8 | ||
![]() |
c130a9f31c | ||
![]() |
622ab922b5 | ||
![]() |
6de2258325 | ||
![]() |
d8e0be69d1 | ||
![]() |
4ee9fdc9fb | ||
![]() |
a8823cc1d1 | ||
![]() |
83ee9e9540 | ||
![]() |
9a74390143 | ||
![]() |
64d6552890 | ||
![]() |
65ebdb4292 | ||
![]() |
7a55abaa42 | ||
![]() |
8f05a639f3 | ||
![]() |
917b467b85 | ||
![]() |
a938001805 | ||
![]() |
9e6de48a22 | ||
![]() |
569aeff054 | ||
![]() |
c86ba49a79 | ||
![]() |
3b4004607d | ||
![]() |
c68e663a1c | ||
![]() |
d870410413 | ||
![]() |
9a8c29e05d | ||
![]() |
a54c8a88ff | ||
![]() |
ca914d8e4f | ||
![]() |
687bedd251 | ||
![]() |
5ddadcbd65 | ||
![]() |
981842ee87 | ||
![]() |
ab69223d75 | ||
![]() |
69f0f38a09 | ||
![]() |
40267760fd | ||
![]() |
6e74b56649 | ||
![]() |
d35802a996 | ||
![]() |
c007286fd6 | ||
![]() |
b5a3cedacd | ||
![]() |
3d53bdc6c5 | ||
![]() |
a7f6a6f22c | ||
![]() |
7893eaa389 | ||
![]() |
e410977e64 | ||
![]() |
d48ca1d858 | ||
![]() |
8758a086c1 | ||
![]() |
1db5c514e6 | ||
![]() |
613aa9b2cf | ||
![]() |
4c6e854cad | ||
![]() |
66a6e55310 | ||
![]() |
bffbd5607b | ||
![]() |
12b5dbdd83 | ||
![]() |
9e7ae1daa4 | ||
![]() |
f36ee88a87 | ||
![]() |
8b3bad1f54 | ||
![]() |
b407792bd1 | ||
![]() |
12376a2338 | ||
![]() |
088cfc3576 | ||
![]() |
01b8f97201 | ||
![]() |
195e34cc09 | ||
![]() |
fbab6741af | ||
![]() |
e2b9e21c6a | ||
![]() |
4a26352c50 | ||
![]() |
d43371ed2f | ||
![]() |
4f24d63de1 | ||
![]() |
cd9339903f | ||
![]() |
ca01bdc481 | ||
![]() |
39a5341ab8 | ||
![]() |
3c93f6e3f9 | ||
![]() |
13ce4322ac | ||
![]() |
b3ba506e6c | ||
![]() |
ea9fc6052d | ||
![]() |
c8ceea4be8 | ||
![]() |
3df993b9a4 | ||
![]() |
980f19173f | ||
![]() |
b1da600269 | ||
![]() |
34c5f79983 | ||
![]() |
4cd3527761 | ||
![]() |
1dbe1955eb | ||
![]() |
743abadfcf | ||
![]() |
bbd223af1f | ||
![]() |
cb717c0ec6 | ||
![]() |
4956cf3727 | ||
![]() |
f76165e761 | ||
![]() |
dd00d0daad | ||
![]() |
77ec87d0ac | ||
![]() |
2209f0b884 | ||
![]() |
61fd073a5c | ||
![]() |
efa7fe0dc9 | ||
![]() |
00a1d9d1b0 | ||
![]() |
630c438834 | ||
![]() |
3ada93b293 | ||
![]() |
291499d5e1 | ||
![]() |
08c453581c | ||
![]() |
5e25bbba2d | ||
![]() |
eb85185072 | ||
![]() |
3f72030d5f | ||
![]() |
69a4d2107f | ||
![]() |
46fe132e83 | ||
![]() |
c60f19b35b | ||
![]() |
ba44986524 | ||
![]() |
73811eac0a | ||
![]() |
3ff3cb975b | ||
![]() |
8ec5472b79 | ||
![]() |
b0415588d7 | ||
![]() |
abcf925b79 | ||
![]() |
37e13505cf | ||
![]() |
5d76d92bcf | ||
![]() |
b71870aba3 | ||
![]() |
734d6cd247 | ||
![]() |
40faa156e2 | ||
![]() |
4737091722 | ||
![]() |
4160ed190c | ||
![]() |
8e74f63d47 | ||
![]() |
fc62bc5fc1 | ||
![]() |
010b4f6b15 | ||
![]() |
b16d4dd94b | ||
![]() |
0813adc327 | ||
![]() |
e68cf80531 | ||
![]() |
258c91d483 | ||
![]() |
64d6101fb7 | ||
![]() |
fb0cb7cad6 | ||
![]() |
a3c0b83dee | ||
![]() |
1ff5dd8ef5 | ||
![]() |
f3f5fca0b9 | ||
![]() |
d15a1a6711 | ||
![]() |
e39c8e350c | ||
![]() |
c1da554eb1 | ||
![]() |
1f1fd8de87 | ||
![]() |
cf6cb0bd39 | ||
![]() |
a8264ae8ae | ||
![]() |
642dc5b49c | ||
![]() |
43ae0f2541 | ||
![]() |
f2233b3034 | ||
![]() |
c3fe5f012e | ||
![]() |
7f9b454922 | ||
![]() |
15915680b5 | ||
![]() |
2e4226d7d3 | ||
![]() |
0cd93e7e65 | ||
![]() |
611d5be40a | ||
![]() |
072bf75d71 | ||
![]() |
f9000ae08c | ||
![]() |
c8183bd35a | ||
![]() |
99f91003d8 | ||
![]() |
ed2024e67a | ||
![]() |
cd91aca3b5 | ||
![]() |
77ea654a1f | ||
![]() |
ef6d3a5236 | ||
![]() |
502574e86f | ||
![]() |
fd1ddbd93d | ||
![]() |
a12bc70543 | ||
![]() |
b84e93f462 | ||
![]() |
f4b0baecd3 | ||
![]() |
f700a1faa3 | ||
![]() |
eb90f5a581 | ||
![]() |
20ce879471 | ||
![]() |
df3688ef08 | ||
![]() |
e76bd1bbb9 | ||
![]() |
d580f8a8a2 | ||
![]() |
ffb485aa87 | ||
![]() |
741cb23776 | ||
![]() |
6afb60d31b | ||
![]() |
761bb65ac6 | ||
![]() |
7464e3944e | ||
![]() |
e2f2c13e5e | ||
![]() |
0ee0b2fcba | ||
![]() |
1e9c585e8b | ||
![]() |
e78f4d2a29 | ||
![]() |
1f6faaacab | ||
![]() |
7e895f7d10 | ||
![]() |
5031ffe767 | ||
![]() |
37fe25cfdc | ||
![]() |
cff7aa229e | ||
![]() |
e09dde2ea9 | ||
![]() |
cadbe885d1 | ||
![]() |
366f592a8a | ||
![]() |
e491629143 | ||
![]() |
f44cb9b03e | ||
![]() |
752c73a2ed | ||
![]() |
a8ecdb3bff | ||
![]() |
9c798cbb5d | ||
![]() |
8938c109c2 | ||
![]() |
85448ea903 | ||
![]() |
e3d2f917e2 | ||
![]() |
05795d0ad8 | ||
![]() |
a38e033e13 | ||
![]() |
8df447091d | ||
![]() |
760f2d1959 | ||
![]() |
e64f76bebe | ||
![]() |
7c5090d627 | ||
![]() |
f6a0d630c3 | ||
![]() |
880f5faeec | ||
![]() |
0cf503d871 | ||
![]() |
9d050360c8 | ||
![]() |
0c0c61f9e0 | ||
![]() |
e868b3e8ff | ||
![]() |
555215a848 | ||
![]() |
484a547758 | ||
![]() |
7d25f68fa5 | ||
![]() |
8b22ab93c1 | ||
![]() |
78e3a2d0c6 | ||
![]() |
241c89e885 | ||
![]() |
7d96a2a620 | ||
![]() |
08104eec56 | ||
![]() |
0fc81d6b33 | ||
![]() |
cb84e55c34 | ||
![]() |
68c3d5a159 | ||
![]() |
77bab39ed0 | ||
![]() |
92e570ffc1 | ||
![]() |
919684e20a | ||
![]() |
a1d6df6ce9 | ||
![]() |
07c3c3bba8 | ||
![]() |
f11e040662 | ||
![]() |
8d83341308 | ||
![]() |
f27b2c4df1 | ||
![]() |
717b84bab9 | ||
![]() |
a34bce6202 | ||
![]() |
bd190b9b4c | ||
![]() |
da6c6c5201 | ||
![]() |
f50afae1c3 | ||
![]() |
177afea5ad | ||
![]() |
a3aae68229 | ||
![]() |
9ff9d9230e | ||
![]() |
2bb0843c30 | ||
![]() |
5f2425f421 | ||
![]() |
e46ca41697 | ||
![]() |
fa5a7aea7e | ||
![]() |
030681a443 | ||
![]() |
aa3cbf2473 | ||
![]() |
ce71f6444c | ||
![]() |
eb4d561b96 | ||
![]() |
075a41c69a | ||
![]() |
2ba2248f67 | ||
![]() |
ff5ed82de8 | ||
![]() |
541b969d3b | ||
![]() |
3d83c6299b | ||
![]() |
3ecde49dca | ||
![]() |
c1fcd8ea7f | ||
![]() |
78ac8ba841 | ||
![]() |
d9cfab4c8e | ||
![]() |
4c10502b0e | ||
![]() |
a576f7baf3 | ||
![]() |
520c964656 | ||
![]() |
3f59b1c376 | ||
![]() |
3ff095cc51 | ||
![]() |
aa4c41abe8 | ||
![]() |
906b3901fb | ||
![]() |
2aba4f261f | ||
![]() |
3eb0c8ddff | ||
![]() |
705a987547 | ||
![]() |
888f17c504 | ||
![]() |
2f4d0ede0f | ||
![]() |
6fd9857666 | ||
![]() |
f07265ece4 | ||
![]() |
a169d6ca97 | ||
![]() |
ebed38c1dc | ||
![]() |
5619042fe7 | ||
![]() |
67b3428b07 | ||
![]() |
2302a3bb33 | ||
![]() |
a83eafd949 | ||
![]() |
2956f4fea1 | ||
![]() |
180e1f462c | ||
![]() |
2dc63eb8c5 | ||
![]() |
4c40ec4948 | ||
![]() |
56b3dc02a7 | ||
![]() |
f66feabaaf | ||
![]() |
0ef098a9f3 | ||
![]() |
db5bcd9fc4 | ||
![]() |
c845f4e9b2 | ||
![]() |
5aff3499a0 | ||
![]() |
a501451038 | ||
![]() |
0deed82bea | ||
![]() |
f9231de824 | ||
![]() |
757c66613d | ||
![]() |
9d2302f2f5 | ||
![]() |
0bbbd2cd54 | ||
![]() |
02b028add3 | ||
![]() |
34455f9743 | ||
![]() |
8c4eec231f | ||
![]() |
621a14d7cc | ||
![]() |
4906e78a5c | ||
![]() |
146e440d59 | ||
![]() |
e2ede3ed19 | ||
![]() |
b76ac68fb1 | ||
![]() |
0691ad9362 | ||
![]() |
715f116954 | ||
![]() |
9f0db98745 | ||
![]() |
0ba55c31e8 | ||
![]() |
19b7cfbd4a | ||
![]() |
a9520888cf | ||
![]() |
f086f4a955 | ||
![]() |
a657964c25 | ||
![]() |
543104b36c | ||
![]() |
bf1d2069e4 | ||
![]() |
e5e1c9fb05 | ||
![]() |
4c4be88323 | ||
![]() |
5a83627dc5 | ||
![]() |
3123a7b168 | ||
![]() |
8161ce6ea8 | ||
![]() |
d9cbd1b65f | ||
![]() |
b7c07209b8 | ||
![]() |
6c3a4f17f0 | ||
![]() |
d82feb807f | ||
![]() |
c373fa9296 | ||
![]() |
139b48440f | ||
![]() |
9de1d3b143 | ||
![]() |
b69ebdaecb | ||
![]() |
f25e50b017 | ||
![]() |
a4a7601f9f | ||
![]() |
41a503f76f | ||
![]() |
f1a3d62db2 | ||
![]() |
e465276464 | ||
![]() |
dbc15a2dda | ||
![]() |
47b45444eb | ||
![]() |
cf0911cc56 | ||
![]() |
da79d5b2e3 | ||
![]() |
358b0c1c17 | ||
![]() |
543348fe58 | ||
![]() |
0635856761 | ||
![]() |
081afe6034 | ||
![]() |
ca14322227 | ||
![]() |
a54816a6e5 | ||
![]() |
27db4e90b5 | ||
![]() |
e9cc624d93 | ||
![]() |
5a95f43992 | ||
![]() |
36a35132c0 | ||
![]() |
2fbc75f89b | ||
![]() |
48aa6be889 | ||
![]() |
bde04bc47b | ||
![]() |
7d163aa659 | ||
![]() |
010b044379 | ||
![]() |
7fefd58b84 | ||
![]() |
87b60967a6 | ||
![]() |
e80069545f | ||
![]() |
be5685695e | ||
![]() |
6b769ac263 | ||
![]() |
9114816384 | ||
![]() |
db3e596e48 | ||
![]() |
bdc21da076 | ||
![]() |
a500eeb831 | ||
![]() |
119d0c576a | ||
![]() |
38cee53999 | ||
![]() |
2ca9d4689e | ||
![]() |
8a32ffc7b9 | ||
![]() |
6475b1a446 | ||
![]() |
07db244f91 | ||
![]() |
ff4aed1f6e | ||
![]() |
3208815e10 | ||
![]() |
b4a1bdcb83 | ||
![]() |
97869636f8 | ||
![]() |
cbb092f792 | ||
![]() |
0c5ee37721 | ||
![]() |
e74aeeab1a | ||
![]() |
b8df9c7e97 | ||
![]() |
82a9e67b7e | ||
![]() |
7410b8778a | ||
![]() |
3e92f23680 | ||
![]() |
3942e6a841 | ||
![]() |
e76b483067 | ||
![]() |
3de740ed1e | ||
![]() |
bbe975baef | ||
![]() |
6dff975711 | ||
![]() |
71108d9ca0 | ||
![]() |
053e5417a7 | ||
![]() |
9bbc49e842 | ||
![]() |
270780ef5f | ||
![]() |
e15963b422 | ||
![]() |
52e8196d0a | ||
![]() |
cc62943835 | ||
![]() |
d195726ed2 | ||
![]() |
50e6c83dd8 | ||
![]() |
3a58d97496 | ||
![]() |
ace12958d1 | ||
![]() |
d33a0f75fd | ||
![]() |
d24a60777b | ||
![]() |
f2a3a5cbbd | ||
![]() |
3bf9908789 | ||
![]() |
912798ee34 | ||
![]() |
28990e1db5 | ||
![]() |
e8281bb009 | ||
![]() |
334f9deaec | ||
![]() |
1d47dc41c9 | ||
![]() |
66ecc4d69d | ||
![]() |
fa3edb5c01 | ||
![]() |
ea046f32be | ||
![]() |
fd09476b28 | ||
![]() |
7c306acd5d | ||
![]() |
9c4733595a | ||
![]() |
c7cf9585ae | ||
![]() |
301ca88f41 | ||
![]() |
9a0fed89bd | ||
![]() |
2050b0b375 | ||
![]() |
34c7c3f384 | ||
![]() |
3b9d8e00bc | ||
![]() |
6b35b069b2 | ||
![]() |
9428127021 | ||
![]() |
1e8843947c | ||
![]() |
dbdffbba23 | ||
![]() |
460f02ede5 | ||
![]() |
0eb6c88bc5 | ||
![]() |
4b7650f2d2 | ||
![]() |
8004c6605b | ||
![]() |
9d451b6358 | ||
![]() |
7963665c40 | ||
![]() |
d44a34ce1e | ||
![]() |
49b7559b1f | ||
![]() |
43b1dd64a7 | ||
![]() |
2d0c1fac24 | ||
![]() |
a0f35a84ae | ||
![]() |
4bc5987f36 | ||
![]() |
11644d48ee | ||
![]() |
d273a92a19 | ||
![]() |
b0ff4b5841 | ||
![]() |
ef99658919 | ||
![]() |
a9238c7577 | ||
![]() |
993e98a43f | ||
![]() |
10dd11f257 | ||
![]() |
fb9be3da79 | ||
![]() |
3b1a33d606 | ||
![]() |
710e18f399 | ||
![]() |
67b9904740 | ||
![]() |
e413e9b93b | ||
![]() |
5c86042b31 | ||
![]() |
e89333811e | ||
![]() |
4f723232e3 | ||
![]() |
48520d90ef | ||
![]() |
2fdda91cb8 | ||
![]() |
c023f610dd | ||
![]() |
161b62d8fa | ||
![]() |
8ccedd4064 | ||
![]() |
9a06584a1d | ||
![]() |
a21e586140 | ||
![]() |
91f01d660f | ||
![]() |
1748dbd60f | ||
![]() |
5acae7f86d | ||
![]() |
30ecba9944 | ||
![]() |
4287df5f3d | ||
![]() |
063deab3cb | ||
![]() |
27798a6004 | ||
![]() |
577ddd9021 | ||
![]() |
34663e160d | ||
![]() |
ac54b81289 | ||
![]() |
67174fb07e | ||
![]() |
d2a692393f | ||
![]() |
9aa2664188 | ||
![]() |
ab5d60e33d | ||
![]() |
31847d8cfb | ||
![]() |
9729f1f38b | ||
![]() |
6bc6733c40 | ||
![]() |
b1ffcb4245 | ||
![]() |
f0c5fbfb8a | ||
![]() |
c76239806d | ||
![]() |
6d809b0b5a | ||
![]() |
de2cbb7f5c | ||
![]() |
cd61f37df7 | ||
![]() |
26796f87cd | ||
![]() |
e2dd897ac7 | ||
![]() |
3bbe4baaf7 | ||
![]() |
d409b86217 | ||
![]() |
7928c15849 | ||
![]() |
d197debbc0 | ||
![]() |
55b9dee448 | ||
![]() |
5c6984d326 | ||
![]() |
a7787d6080 | ||
![]() |
2db60340c2 | ||
![]() |
c121631fef | ||
![]() |
b0fb16d48d | ||
![]() |
3e07f6543e | ||
![]() |
d4c2356c70 | ||
![]() |
eec617b391 | ||
![]() |
b15c9ad130 | ||
![]() |
0128d85999 | ||
![]() |
e69ca0cf80 | ||
![]() |
0719753be3 | ||
![]() |
ba3181d4e7 | ||
![]() |
e58750555e | ||
![]() |
026687299d | ||
![]() |
3eed552c56 | ||
![]() |
15a4514c7d | ||
![]() |
b5445c0061 | ||
![]() |
1d0584a90d | ||
![]() |
158b795c70 | ||
![]() |
4994229215 | ||
![]() |
c022c32d2f | ||
![]() |
d2ef3ca100 | ||
![]() |
00faadcfea | ||
![]() |
a6ff52b300 | ||
![]() |
da0d65ca5b | ||
![]() |
2266e97417 | ||
![]() |
d471de5645 | ||
![]() |
38674f0dc2 | ||
![]() |
b192ca4bad | ||
![]() |
73a59523f5 | ||
![]() |
05324dedd0 | ||
![]() |
f1e5f73d7e | ||
![]() |
7b23f21712 | ||
![]() |
4dde314338 | ||
![]() |
cba12fb598 | ||
![]() |
63e38b4d8d | ||
![]() |
7eded95315 | ||
![]() |
e493fe1105 | ||
![]() |
646c230940 | ||
![]() |
5276a3688e | ||
![]() |
0616bf16f4 | ||
![]() |
fbe1811e2b | ||
![]() |
2333c10915 | ||
![]() |
77e9142722 | ||
![]() |
943998e57e | ||
![]() |
58802b71c4 | ||
![]() |
ca89aa7a94 | ||
![]() |
4faa920318 | ||
![]() |
b394c07a3d | ||
![]() |
554cb27703 | ||
![]() |
80a04314fc | ||
![]() |
6516cd388f | ||
![]() |
4f6141581e | ||
![]() |
597c386bc2 | ||
![]() |
494c7aa3da | ||
![]() |
8840970d64 | ||
![]() |
867624fc59 | ||
![]() |
ea4120a7d4 | ||
![]() |
158bbf1f52 | ||
![]() |
61f8970aca | ||
![]() |
6f41fbeb22 | ||
![]() |
a540c62594 | ||
![]() |
3e6a216806 | ||
![]() |
85535b2cbd | ||
![]() |
05796dcd51 | ||
![]() |
40e2c7b9b7 | ||
![]() |
d0fe7de501 | ||
![]() |
0dadd31221 | ||
![]() |
09515bf174 | ||
![]() |
773a2a9db6 | ||
![]() |
31a576b206 | ||
![]() |
58161b5fa2 | ||
![]() |
ebb61caa53 | ||
![]() |
54a7691a80 | ||
![]() |
996839cb67 | ||
![]() |
e065f1e097 | ||
![]() |
882565a8e5 | ||
![]() |
4501303beb | ||
![]() |
1416580f8b | ||
![]() |
5e58032745 | ||
![]() |
86cf01a901 | ||
![]() |
45c0a19a68 | ||
![]() |
35ab2a21d6 | ||
![]() |
977d2fe8b3 | ||
![]() |
626f8a9166 | ||
![]() |
1654249dab | ||
![]() |
5fadc56475 | ||
![]() |
2bce697aa7 | ||
![]() |
970edbed40 | ||
![]() |
131ba3cdef | ||
![]() |
85f1c89808 | ||
![]() |
2940cb0fa0 | ||
![]() |
ba8d40f7d3 | ||
![]() |
cac0e0f6e8 | ||
![]() |
ad7cfe49c8 | ||
![]() |
e29fc37bb1 | ||
![]() |
e892744328 | ||
![]() |
356775c19b | ||
![]() |
87bd6e3ca0 | ||
![]() |
ad6f66c945 | ||
![]() |
9537229c92 | ||
![]() |
c18b6d736a | ||
![]() |
a7afeb078c | ||
![]() |
00627b82e0 | ||
![]() |
9a2f17c2b2 | ||
![]() |
4cecb6c851 | ||
![]() |
b6c4b06fc7 | ||
![]() |
3e0e807c96 | ||
![]() |
7dad6ebe67 | ||
![]() |
75b8cb19cf | ||
![]() |
bd28452807 | ||
![]() |
ed6cfa42f0 | ||
![]() |
13aba6201e | ||
![]() |
f392e0c1c7 | ||
![]() |
181eca6c82 | ||
![]() |
196d923ac6 | ||
![]() |
4ad387c967 | ||
![]() |
cb475bf153 | ||
![]() |
47acceea08 | ||
![]() |
fd6fb7e3bc | ||
![]() |
30f7e9b441 | ||
![]() |
a8beec2691 | ||
![]() |
23244fb79f | ||
![]() |
e5c56629e2 | ||
![]() |
a793503c8a | ||
![]() |
054c7a0adc | ||
![]() |
6eb2d1aa7c | ||
![]() |
619fdea5df | ||
![]() |
763f2bcfcc | ||
![]() |
9e3684b001 | ||
![]() |
93fd82d1fa | ||
![]() |
9757009d8f | ||
![]() |
920d281d45 | ||
![]() |
d2bdc85a7b | ||
![]() |
1f84c5e1f1 | ||
![]() |
a93bf3c150 | ||
![]() |
7bad07ac10 | ||
![]() |
af019144e5 | ||
![]() |
e69b3ebf1e | ||
![]() |
4271d3f32f | ||
![]() |
d6e5fdceb7 | ||
![]() |
c4ceb4759a | ||
![]() |
6350ed3415 | ||
![]() |
031b25cd1e | ||
![]() |
47455fee41 | ||
![]() |
9abb4ffc97 | ||
![]() |
90a7ecdce3 | ||
![]() |
a84b8b49f3 | ||
![]() |
ff6f213664 | ||
![]() |
e4b686bc43 | ||
![]() |
307bb05653 | ||
![]() |
b4ae08f83d | ||
![]() |
21e2bbd066 | ||
![]() |
0d85cec770 | ||
![]() |
a1e6f596d7 | ||
![]() |
eab1d5717f | ||
![]() |
19b1dc8d65 | ||
![]() |
2c8e33558e | ||
![]() |
7287f302f6 | ||
![]() |
1342dc142c | ||
![]() |
96a8902365 | ||
![]() |
d1b85cd452 | ||
![]() |
8977458e48 | ||
![]() |
a37f8b1f4e | ||
![]() |
bdf4a21976 | ||
![]() |
1322d54371 | ||
![]() |
fbe63e8d03 | ||
![]() |
6b2a4c975c | ||
![]() |
b1392e1fc8 | ||
![]() |
7100481abc | ||
![]() |
4c43640d0d | ||
![]() |
42f53ff917 | ||
![]() |
e0fb612e82 | ||
![]() |
d13f9be9d8 | ||
![]() |
2396b1e73c | ||
![]() |
374b3ac6c6 | ||
![]() |
5df09c4f13 | ||
![]() |
337c64d69d | ||
![]() |
34dbd1fb10 | ||
![]() |
7ee9f0af2d | ||
![]() |
cb6847b64c | ||
![]() |
04867f6ecc | ||
![]() |
9e94e94075 | ||
![]() |
014c5dc764 | ||
![]() |
a1599d5f7d | ||
![]() |
2fd678bb59 | ||
![]() |
3c4c3dc08e | ||
![]() |
bbc3862fec | ||
![]() |
678e25d0b1 | ||
![]() |
a6f91177b6 | ||
![]() |
ff637ef046 | ||
![]() |
1cb813e0c5 | ||
![]() |
ce4e51078f | ||
![]() |
066d0f4143 | ||
![]() |
1294918f5b | ||
![]() |
4a556f89aa | ||
![]() |
e74a29c87a | ||
![]() |
50d57852a6 | ||
![]() |
744d5f7bd4 | ||
![]() |
0b0a239ed4 | ||
![]() |
4cc538b5ae | ||
![]() |
f5c67e2fd1 | ||
![]() |
9ec5d90f4d | ||
![]() |
e1344fca6c | ||
![]() |
e8bdc7286e | ||
![]() |
e290829bc0 | ||
![]() |
dc0998d95d | ||
![]() |
5456cd0ac1 | ||
![]() |
18f2b120ef | ||
![]() |
43d8345821 | ||
![]() |
1ce44800ab | ||
![]() |
c26b3f519a | ||
![]() |
999e930fc8 | ||
![]() |
d4e99efc46 | ||
![]() |
fb01a0a9f1 | ||
![]() |
9556285c59 | ||
![]() |
2d40b1ec75 | ||
![]() |
7eb690b125 | ||
![]() |
a23644debc | ||
![]() |
2f7fcb4f5e | ||
![]() |
c4e4c52c6c | ||
![]() |
e6912b94df | ||
![]() |
704e4221f7 | ||
![]() |
48a2dde16b | ||
![]() |
293e01f2e9 | ||
![]() |
e2820787bf | ||
![]() |
c98ba7f6ba | ||
![]() |
aa2b61f133 | ||
![]() |
f85d4afe45 | ||
![]() |
b4ab9177b8 | ||
![]() |
e7c310ca58 | ||
![]() |
85a83f2553 | ||
![]() |
d2e7baeb38 | ||
![]() |
07b2ce28b1 | ||
![]() |
ed1eea9b50 | ||
![]() |
f7d8e4e7b9 | ||
![]() |
a2ab28286f | ||
![]() |
99f55665a5 | ||
![]() |
0aa817e300 | ||
![]() |
4cdb7a9887 | ||
![]() |
92a19357d3 | ||
![]() |
dded1305ec | ||
![]() |
d6e85eef48 | ||
![]() |
0b1875de14 | ||
![]() |
c5ef8659a7 | ||
![]() |
9a332f19c2 | ||
![]() |
35c90d9bde | ||
![]() |
a9632bd0ff | ||
![]() |
983e134ae9 | ||
![]() |
e217532f9e | ||
![]() |
65ad39f5be | ||
![]() |
358d904c2c | ||
![]() |
65278100a0 | ||
![]() |
dbffd8c0ff | ||
![]() |
2a25dcd44e | ||
![]() |
6e7f57383a | ||
![]() |
946172d530 | ||
![]() |
2791329460 | ||
![]() |
1eeab28eec | ||
![]() |
2a3bd45901 | ||
![]() |
d16453a465 | ||
![]() |
de63dddc96 | ||
![]() |
ccffe19611 | ||
![]() |
806bcf47d9 | ||
![]() |
320df710a4 | ||
![]() |
76df7de0cf | ||
![]() |
da7e9f3ab6 | ||
![]() |
a673bd7a91 | ||
![]() |
121e9e4e7f | ||
![]() |
452e946509 | ||
![]() |
c3ce82d874 | ||
![]() |
253217958b | ||
![]() |
1447392847 | ||
![]() |
32a6b8a0f8 | ||
![]() |
5ed3f18d70 | ||
![]() |
7cc142dd59 | ||
![]() |
9150c78901 | ||
![]() |
4b7c337dc9 | ||
![]() |
1aa79c71cc | ||
![]() |
5f70140e72 | ||
![]() |
58f7a8a51e | ||
![]() |
a91ae71139 | ||
![]() |
576b4ef60d | ||
![]() |
0ec7dc5654 | ||
![]() |
bdf6f7f590 | ||
![]() |
fbae79fab2 | ||
![]() |
2c34712069 | ||
![]() |
40e3038775 | ||
![]() |
e2c02706a0 | ||
![]() |
313be7b30a | ||
![]() |
d0ed8b67c4 | ||
![]() |
deaaf2f082 | ||
![]() |
ce95876d03 | ||
![]() |
5475d7ef58 | ||
![]() |
687c74ee4c | ||
![]() |
c9a9488ff5 | ||
![]() |
57217b46ed | ||
![]() |
5a01521ff8 | ||
![]() |
19a0a16915 | ||
![]() |
62877c2c58 | ||
![]() |
9479874bb4 | ||
![]() |
241b6a0170 | ||
![]() |
babc183834 | ||
![]() |
f3371bcf39 | ||
![]() |
33da5465bd | ||
![]() |
5df3a9d76d | ||
![]() |
ec4f4a4a1f | ||
![]() |
46df29b390 | ||
![]() |
60846434d3 | ||
![]() |
66c86c0461 | ||
![]() |
73996fb916 | ||
![]() |
0edfbded23 | ||
![]() |
212c3ddcca | ||
![]() |
918499a85c | ||
![]() |
46ef578986 | ||
![]() |
86162eb660 | ||
![]() |
7f7a33b027 | ||
![]() |
867df99353 | ||
![]() |
283e9d073b | ||
![]() |
38f26376a1 | ||
![]() |
0322dd0e0f | ||
![]() |
edcb090209 | ||
![]() |
92010e1fca | ||
![]() |
12f9a11716 | ||
![]() |
0dd21f4c89 | ||
![]() |
14f967cdd0 | ||
![]() |
f3b23afc92 | ||
![]() |
0bf807b96e | ||
![]() |
1879b8c27f | ||
![]() |
3798802557 | ||
![]() |
f7833bdbd4 | ||
![]() |
e3a156c9b7 | ||
![]() |
6247ec73a3 | ||
![]() |
3feda06e60 | ||
![]() |
56e895fdd4 | ||
![]() |
541506cbdb | ||
![]() |
1f4cda6282 | ||
![]() |
6f77d0b0d5 | ||
![]() |
7976e1b104 | ||
![]() |
1c260cfb00 | ||
![]() |
8424f179e4 | ||
![]() |
00a14a0824 | ||
![]() |
34bec1c50f | ||
![]() |
1d0c520f64 | ||
![]() |
d51eda40b3 | ||
![]() |
e3ed9fac78 | ||
![]() |
b98a27d3d0 | ||
![]() |
c73383ded3 | ||
![]() |
36a08d04c5 | ||
![]() |
8a95fffbab | ||
![]() |
633c770a48 | ||
![]() |
826d28974b | ||
![]() |
135df5a24e | ||
![]() |
2e8e13bffb | ||
![]() |
5e8def837e | ||
![]() |
14735cce26 | ||
![]() |
d775e443f8 | ||
![]() |
aa8dfa760d | ||
![]() |
0043b18135 | ||
![]() |
c14ddedfae | ||
![]() |
a073a6b01e | ||
![]() |
0713ac4977 | ||
![]() |
3390dc0dbb | ||
![]() |
445b38f25d | ||
![]() |
9e4a20c267 | ||
![]() |
d88cd72d13 | ||
![]() |
66b2e06cd3 | ||
![]() |
58906008b9 | ||
![]() |
aa062515b8 | ||
![]() |
65da1e79b9 | ||
![]() |
41ecb24135 | ||
![]() |
e3b3c32751 | ||
![]() |
e2a8137140 | ||
![]() |
fa6a2f08ab | ||
![]() |
68d62ab58e | ||
![]() |
c6b9a40234 | ||
![]() |
e0916fdd26 | ||
![]() |
cad2d72ed9 | ||
![]() |
8eaddbf2b2 | ||
![]() |
9b30f32cad | ||
![]() |
c2a69bcb20 | ||
![]() |
2e7b60c3ca | ||
![]() |
eca811d0d4 | ||
![]() |
8e202bc202 | ||
![]() |
429682cecd | ||
![]() |
9cd2080de2 | ||
![]() |
2960271b81 | ||
![]() |
2d3259413a | ||
![]() |
7a7bd9c621 | ||
![]() |
8ce0b6b4b3 | ||
![]() |
63679333cc | ||
![]() |
5b12bdca00 | ||
![]() |
99e13278e3 | ||
![]() |
07a03ee10d | ||
![]() |
fb9f8e3581 | ||
![]() |
ee125cd9a4 | ||
![]() |
35a1429e2b | ||
![]() |
89916b38e9 | ||
![]() |
c560439545 | ||
![]() |
7322be2006 | ||
![]() |
e95ed12ba1 | ||
![]() |
8048d2bfb8 | ||
![]() |
490bb46a82 | ||
![]() |
1199353204 | ||
![]() |
2c368c79d1 | ||
![]() |
095318114b | ||
![]() |
9e388f5b13 | ||
![]() |
87fab1fa14 | ||
![]() |
8046684179 | ||
![]() |
5a475ec7ea | ||
![]() |
8c6edd8b81 | ||
![]() |
de496c693e | ||
![]() |
cb37d4d36a | ||
![]() |
2aa82da615 | ||
![]() |
04982f5e12 | ||
![]() |
b9e11b0f45 | ||
![]() |
1e0d1c46ab | ||
![]() |
b5d499dda8 | ||
![]() |
d1615f9a6e | ||
![]() |
516a3c0504 | ||
![]() |
2a5c0d9b88 | ||
![]() |
a15a3c12d5 | ||
![]() |
a6131b3ebf | ||
![]() |
b9aadb252f | ||
![]() |
1264c2cbfa | ||
![]() |
716b559e5d | ||
![]() |
30e4264aa9 | ||
![]() |
fb94f8ea18 | ||
![]() |
aea5760424 | ||
![]() |
debec3bfbc | ||
![]() |
4122f94fb6 | ||
![]() |
b48a2cf2b5 | ||
![]() |
0ca9ad1cc0 | ||
![]() |
ee555a3700 | ||
![]() |
a2bc3e3908 | ||
![]() |
64b7f2c285 | ||
![]() |
db2435dc36 | ||
![]() |
1d500fda67 | ||
![]() |
16f36912db | ||
![]() |
2a5f031ba5 | ||
![]() |
71bb8ae529 | ||
![]() |
f6a94d0661 | ||
![]() |
c2575735ff | ||
![]() |
7eee5ecd9a | ||
![]() |
db0cf9fbf4 | ||
![]() |
aded44ee0f | ||
![]() |
901926e8e6 | ||
![]() |
2e336626ac | ||
![]() |
3ea3d77f4d | ||
![]() |
fe8e7b73bf | ||
![]() |
a34065ee2f | ||
![]() |
e1a908c8ac | ||
![]() |
628d99886a | ||
![]() |
6b10710484 | ||
![]() |
8a4f28fa94 | ||
![]() |
558b0ec3b1 | ||
![]() |
9780db1c22 | ||
![]() |
5e39fb6da1 | ||
![]() |
4450f919c3 | ||
![]() |
3183bb78ff | ||
![]() |
e74f918382 | ||
![]() |
247d2e7efd | ||
![]() |
32b7edb608 | ||
![]() |
df4297be62 | ||
![]() |
4c2e9fc759 | ||
![]() |
2890fc7dd2 | ||
![]() |
97be2c4ac9 | ||
![]() |
762d284102 | ||
![]() |
4967c287f8 | ||
![]() |
5e463d6af4 | ||
![]() |
cbf4676ae4 | ||
![]() |
81444c8f4a | ||
![]() |
9861bd88b9 | ||
![]() |
b0f1c71129 | ||
![]() |
86b845f04a | ||
![]() |
3af0d6e484 | ||
![]() |
fca62f1ae8 | ||
![]() |
4e8d68a2ef | ||
![]() |
e7331633c7 | ||
![]() |
934be08a59 | ||
![]() |
485522fd76 | ||
![]() |
eba0daa2e9 | ||
![]() |
851779e7ad | ||
![]() |
ea9a0f4bf5 | ||
![]() |
1143468eb5 | ||
![]() |
52b0b1e2ab | ||
![]() |
23ba652b83 | ||
![]() |
43b737c4a2 | ||
![]() |
3fbd23b98d | ||
![]() |
0cbeeebd0b | ||
![]() |
99a0679ee9 | ||
![]() |
e82713b68c | ||
![]() |
9293afd95a | ||
![]() |
9ea3e786f6 | ||
![]() |
a8169d2056 | ||
![]() |
1cd94affd1 | ||
![]() |
883ab44437 | ||
![]() |
abd17d9af9 | ||
![]() |
a906a1754e | ||
![]() |
255beafe08 | ||
![]() |
e2679004a1 | ||
![]() |
06bb692522 | ||
![]() |
71599b8e75 | ||
![]() |
79f8bea48d | ||
![]() |
82b335a2c1 | ||
![]() |
361d93eb96 | ||
![]() |
bab699eb0c | ||
![]() |
b8881ed85b | ||
![]() |
4013b418dd | ||
![]() |
80d714b865 | ||
![]() |
7fcad580cb | ||
![]() |
60b6ff4064 | ||
![]() |
24252edf38 | ||
![]() |
79aa7aacec | ||
![]() |
92944fa509 | ||
![]() |
c0f0a4a1ac | ||
![]() |
a084b9fdde | ||
![]() |
83b9b8b032 | ||
![]() |
bc47049d42 | ||
![]() |
17360ede28 | ||
![]() |
f441f4d7c0 | ||
![]() |
5ddc449247 | ||
![]() |
dd8d714c94 | ||
![]() |
c2079ddf6f | ||
![]() |
5250590b17 | ||
![]() |
93f4f14b2a | ||
![]() |
ba712ed514 | ||
![]() |
6e76ca0fb3 | ||
![]() |
b0345cce68 | ||
![]() |
c4eddc8d11 | ||
![]() |
7d89804a87 | ||
![]() |
b92f718e08 | ||
![]() |
ad0209a4a0 | ||
![]() |
d23d25c6b7 | ||
![]() |
724825d34c | ||
![]() |
c3abf5a190 | ||
![]() |
621cf6ce58 | ||
![]() |
83e0ed7b05 | ||
![]() |
0752807aaf | ||
![]() |
53df69ee6e | ||
![]() |
dbc38cdc6b | ||
![]() |
102d55ec57 | ||
![]() |
3f7cae8583 | ||
![]() |
a3a1d424c6 | ||
![]() |
2cede8fec6 | ||
![]() |
5816d495e3 | ||
![]() |
24803b1e75 | ||
![]() |
9732b8c0dd | ||
![]() |
1626b3b7c9 | ||
![]() |
8760a82dfa | ||
![]() |
0f5d5ab0a2 | ||
![]() |
e05f7a9633 | ||
![]() |
30656a4e72 | ||
![]() |
5ccb9486e0 | ||
![]() |
a6d5891e8a | ||
![]() |
fc440f310b | ||
![]() |
4d9ab42ab5 | ||
![]() |
e53f380710 | ||
![]() |
6a514ac2de | ||
![]() |
02bd8d67c8 | ||
![]() |
2c118d4850 | ||
![]() |
949225ffeb | ||
![]() |
1ef04a8dde | ||
![]() |
5c58f97e57 | ||
![]() |
f1b8c8855e | ||
![]() |
daf143f66e | ||
![]() |
6c0e46f050 | ||
![]() |
70133da025 | ||
![]() |
837592381a | ||
![]() |
101b073793 | ||
![]() |
df5f150531 | ||
![]() |
4061314cd2 | ||
![]() |
819be719ef | ||
![]() |
80e4f19172 | ||
![]() |
5b0ea21607 | ||
![]() |
84634ce288 | ||
![]() |
fa1dc75517 | ||
![]() |
d8122d149b | ||
![]() |
923300f4e7 | ||
![]() |
af66d0b647 | ||
![]() |
8b9c4dadd0 | ||
![]() |
857db679ae | ||
![]() |
03ecd7f06c | ||
![]() |
c6bdee8dd8 | ||
![]() |
800f403643 | ||
![]() |
9b1c6b07f5 | ||
![]() |
f7c1a0c5e6 | ||
![]() |
57a7c26c64 | ||
![]() |
d606e86b47 | ||
![]() |
f7a9319122 | ||
![]() |
b16151ac6d | ||
![]() |
bdd9099294 | ||
![]() |
d924f0b1d6 | ||
![]() |
5dab9ba01b | ||
![]() |
ae118da5a1 | ||
![]() |
e24082be9a | ||
![]() |
8fafbfaf82 | ||
![]() |
6168fe006e | ||
![]() |
6c633668f6 | ||
![]() |
73a1dbffeb | ||
![]() |
40217e764d | ||
![]() |
a7af0eaccd | ||
![]() |
4ac29c6aef | ||
![]() |
ef023f084b | ||
![]() |
441bca5bda | ||
![]() |
a8bee20aa3 | ||
![]() |
04bea9c732 | ||
![]() |
98cbc2a182 | ||
![]() |
09518b1a71 | ||
![]() |
42d22bb1a3 | ||
![]() |
69c387a360 | ||
![]() |
4b6fa12925 | ||
![]() |
c562cba030 | ||
![]() |
40764b6995 | ||
![]() |
4ee3290929 | ||
![]() |
dc02c37413 | ||
![]() |
34becb541a | ||
![]() |
eabf88e3c9 | ||
![]() |
c3dac50f21 | ||
![]() |
f7240b52c5 | ||
![]() |
2112b5a763 | ||
![]() |
03b10b45c4 | ||
![]() |
62361230f3 | ||
![]() |
653306eb91 | ||
![]() |
07e2cfb736 | ||
![]() |
f980434046 | ||
![]() |
97084e9382 | ||
![]() |
9db34fe232 | ||
![]() |
c4f0b4ab23 | ||
![]() |
1647afc58a | ||
![]() |
53ea8422f8 | ||
![]() |
0b988b3fac | ||
![]() |
5a4abe3ec1 | ||
![]() |
89abc5ac69 | ||
![]() |
08fe6653bb | ||
![]() |
9aa18c7157 | ||
![]() |
cc7929f8fb | ||
![]() |
d657298791 | ||
![]() |
05f393560f | ||
![]() |
92da640d4c | ||
![]() |
ad3fd151aa | ||
![]() |
cd104dc08c | ||
![]() |
d3745d2519 | ||
![]() |
931f3fa41a | ||
![]() |
87b5a91212 | ||
![]() |
3b8da62d84 | ||
![]() |
86a48294f4 | ||
![]() |
a03884981f | ||
![]() |
ab695f90c7 | ||
![]() |
efcf8f9555 | ||
![]() |
f71903a563 | ||
![]() |
95552e9a5b | ||
![]() |
5da57271b2 | ||
![]() |
62a7139f4d | ||
![]() |
a7be26cd95 | ||
![]() |
9c3b0952e0 | ||
![]() |
c771f446b4 | ||
![]() |
9a25561017 | ||
![]() |
bd870f0537 | ||
![]() |
d7f43bddfa | ||
![]() |
87107c5a59 | ||
![]() |
9ce920b35a | ||
![]() |
15aff9662c | ||
![]() |
da6fb91886 | ||
![]() |
1e880f7406 | ||
![]() |
81153042d3 | ||
![]() |
493ca261dc | ||
![]() |
7493b340ca | ||
![]() |
e85e60ed6a | ||
![]() |
8ff4d5dcbf | ||
![]() |
f2838e493b | ||
![]() |
a71edcf1a1 | ||
![]() |
47bef74e7c | ||
![]() |
b757a7e3fe | ||
![]() |
362ff5724d | ||
![]() |
4f8363a5c2 | ||
![]() |
ae3925118c | ||
![]() |
b2fcab20a6 | ||
![]() |
6423957d29 | ||
![]() |
835cdad0a9 | ||
![]() |
d8d6decb38 | ||
![]() |
16b42cc109 | ||
![]() |
a47f27821f | ||
![]() |
c797e7a973 | ||
![]() |
245eb64405 | ||
![]() |
a895fcf057 | ||
![]() |
5706fb26b8 | ||
![]() |
3f82120cdc | ||
![]() |
20df183470 | ||
![]() |
980216795f | ||
![]() |
fdfcd841ba | ||
![]() |
28a09794e9 | ||
![]() |
a0c9217375 | ||
![]() |
469176c59b | ||
![]() |
3ece672890 | ||
![]() |
c6ebba8843 | ||
![]() |
1f047807a4 | ||
![]() |
f1b724c49a | ||
![]() |
5ebed2046c | ||
![]() |
d1236a53b8 | ||
![]() |
84f07ee992 | ||
![]() |
d7f5e48626 | ||
![]() |
56e07bb1f2 | ||
![]() |
45b2700375 | ||
![]() |
d860b35f41 | ||
![]() |
5392062edd | ||
![]() |
d9a09a2aea | ||
![]() |
3daff73d36 | ||
![]() |
e6b88ec087 | ||
![]() |
592dcec852 | ||
![]() |
2a6b79ec0f | ||
![]() |
afc1d224a0 | ||
![]() |
b668acda24 | ||
![]() |
c3996d6931 | ||
![]() |
9ec174776c | ||
![]() |
6a8722cf7c | ||
![]() |
000b1d80b0 | ||
![]() |
6d8654610e | ||
![]() |
5cd4c8e896 | ||
![]() |
cec8db173b | ||
![]() |
dd9dad80be | ||
![]() |
9992ade051 | ||
![]() |
36da4a9b72 | ||
![]() |
3fc34244ac | ||
![]() |
753c07e911 | ||
![]() |
d0850e2931 | ||
![]() |
c704df004a | ||
![]() |
d95c9c496e | ||
![]() |
d28f4ed618 | ||
![]() |
7a0580eff5 | ||
![]() |
f94af84f2a | ||
![]() |
31fb199670 | ||
![]() |
a1ca0a1cb2 | ||
![]() |
2326c23133 | ||
![]() |
d4c1d1bdb9 | ||
![]() |
8d258871ff | ||
![]() |
49299a6bf0 | ||
![]() |
868b8ad318 | ||
![]() |
40752dcfb6 | ||
![]() |
18f51abfe6 | ||
![]() |
3e2c54dcbd | ||
![]() |
a0cd14b4e8 | ||
![]() |
35c6fdbce8 | ||
![]() |
202addc39d | ||
![]() |
d8cb7c475b | ||
![]() |
03bacd747e | ||
![]() |
97b6a68cda | ||
![]() |
eee18035cf | ||
![]() |
f1b3b0c155 | ||
![]() |
f5d3495c62 | ||
![]() |
e14a356c24 | ||
![]() |
4e7d396e5b | ||
![]() |
34d17ca458 | ||
![]() |
03950f270a | ||
![]() |
7074331461 | ||
![]() |
4c9cd70f65 | ||
![]() |
7a105de969 | ||
![]() |
eec9a28fe8 | ||
![]() |
963f1b1907 | ||
![]() |
dcac9b5f20 | ||
![]() |
765a95c273 | ||
![]() |
6a115d0133 | ||
![]() |
a057effad5 | ||
![]() |
94b0800989 | ||
![]() |
a783b6a0ab | ||
![]() |
5302964eb6 | ||
![]() |
261dbd16a6 | ||
![]() |
672dbc03c6 | ||
![]() |
ed0bdf9e5f | ||
![]() |
735e2e4192 | ||
![]() |
0aabb11220 | ||
![]() |
09ad14bc28 | ||
![]() |
d61e39743b | ||
![]() |
ea90df434b | ||
![]() |
67fc682df2 | ||
![]() |
381b495efc | ||
![]() |
812db815f1 | ||
![]() |
24ee19f1e2 | ||
![]() |
f72c5ebb76 | ||
![]() |
1075ea1220 | ||
![]() |
ce7edca136 | ||
![]() |
3e16857a1e | ||
![]() |
5b1e32f51d | ||
![]() |
4adf5ce826 | ||
![]() |
4a1905a2a2 | ||
![]() |
59af3a396c | ||
![]() |
7c584ece23 | ||
![]() |
ff2c901930 | ||
![]() |
dc8e1773f1 | ||
![]() |
b0d9a2437d | ||
![]() |
2be6ecd50f | ||
![]() |
fa0bb35e6c | ||
![]() |
360bffa3a9 | ||
![]() |
5b503f21d7 | ||
![]() |
2214d9b330 | ||
![]() |
6a2d733d85 | ||
![]() |
7392d5a30a | ||
![]() |
b3deeca939 | ||
![]() |
c38a3a239c | ||
![]() |
afa6ed09ef | ||
![]() |
deb966128f | ||
![]() |
73707fa231 | ||
![]() |
10ac39f6b2 | ||
![]() |
2e05dc8618 | ||
![]() |
d8233b4de5 | ||
![]() |
7cbc3ea65f | ||
![]() |
cb0523660d | ||
![]() |
605bf7e287 | ||
![]() |
3405b2549b | ||
![]() |
d83c617566 | ||
![]() |
7016c19b2f | ||
![]() |
5cd4a0ced6 | ||
![]() |
347c1a2141 | ||
![]() |
46eae64ef6 | ||
![]() |
a74fe60b91 | ||
![]() |
fab70a80bb | ||
![]() |
2abe2f7d59 | ||
![]() |
cc970354d7 | ||
![]() |
e389ff2537 | ||
![]() |
088f0c82bd | ||
![]() |
fa1bb27dd2 | ||
![]() |
5a6ce34352 | ||
![]() |
fdcb88977a | ||
![]() |
a584ccb8f7 | ||
![]() |
cc290b15f6 | ||
![]() |
575db4665d | ||
![]() |
a61aff8432 | ||
![]() |
3aa1c60fe3 | ||
![]() |
39f3aa7e78 | ||
![]() |
01e2c3272b | ||
![]() |
5afcd3e54e | ||
![]() |
b081064954 | ||
![]() |
11e63ca96a | ||
![]() |
6457d46107 | ||
![]() |
987bf4d850 | ||
![]() |
fa80c0a88d | ||
![]() |
f69484ba02 | ||
![]() |
11f63c7868 | ||
![]() |
3245124553 | ||
![]() |
44475967eb | ||
![]() |
2d27b5ac53 | ||
![]() |
2ae161d8b5 | ||
![]() |
aefe83b1a3 | ||
![]() |
f86e85b931 | ||
![]() |
993ebc9eba | ||
![]() |
1d99bbf22e | ||
![]() |
eb4fa635bf | ||
![]() |
49522d93df | ||
![]() |
9e0a7122f5 | ||
![]() |
e4fe7ba985 | ||
![]() |
f3ea11bbc1 | ||
![]() |
55de91530d | ||
![]() |
290bbcfa3e | ||
![]() |
061a1be2bc | ||
![]() |
4bd8c319dd | ||
![]() |
367022dd8c | ||
![]() |
f1975d9dbf | ||
![]() |
0764cf1165 | ||
![]() |
a55a6e5c48 | ||
![]() |
5230aa8917 | ||
![]() |
f7e3e207b7 | ||
![]() |
6f0c59f1be | ||
![]() |
5fcdbd7742 | ||
![]() |
4173ff5339 | ||
![]() |
e8c4d08b25 | ||
![]() |
36081c69e0 | ||
![]() |
65db3c1164 | ||
![]() |
11f02e48d7 | ||
![]() |
e41283a40a | ||
![]() |
738e39413d | ||
![]() |
8df0a950f7 | ||
![]() |
199a274c80 | ||
![]() |
731d1ab796 | ||
![]() |
f6d8868eb6 | ||
![]() |
253cc377b4 | ||
![]() |
1bfd585f3c | ||
![]() |
839eb0fe14 | ||
![]() |
3dcd06806d | ||
![]() |
3c174ce329 | ||
![]() |
8a2347539c | ||
![]() |
f22eca3d9e | ||
![]() |
3cb301214f | ||
![]() |
8215faea0d | ||
![]() |
beab4e0d7c | ||
![]() |
b785d5297a | ||
![]() |
d86d7b8843 | ||
![]() |
2ca5f05555 | ||
![]() |
e95188059f | ||
![]() |
73e6c8541c | ||
![]() |
6f9c8b2aa0 | ||
![]() |
2d20df37b1 | ||
![]() |
0b2e5cd253 | ||
![]() |
0208188bb5 | ||
![]() |
44ae596929 | ||
![]() |
db0cbf1ea9 | ||
![]() |
896da4abbd | ||
![]() |
731e9bbbfc | ||
![]() |
bf1c138a3c | ||
![]() |
00fc3e2c29 | ||
![]() |
a258aa50a5 | ||
![]() |
3cf12a4792 | ||
![]() |
e56f6fafdc | ||
![]() |
e9789e0b3e | ||
![]() |
8aa30b0ccb | ||
![]() |
871a7c87bf | ||
![]() |
72337e4c77 | ||
![]() |
aedd60e74f | ||
![]() |
9249ea0dbb | ||
![]() |
357ec7034e | ||
![]() |
fa4e0519fa | ||
![]() |
ccd1a08aca | ||
![]() |
c654936a91 | ||
![]() |
6c7317fbc3 | ||
![]() |
159e55296f | ||
![]() |
8a084599d8 | ||
![]() |
24b51e0582 | ||
![]() |
2e2faeb612 | ||
![]() |
d3a8af9ed0 | ||
![]() |
def11f9959 | ||
![]() |
0b64151ae0 | ||
![]() |
a86c6e0809 | ||
![]() |
88821b1d0e | ||
![]() |
73f636c40d | ||
![]() |
042e11b1d7 | ||
![]() |
06cc505956 | ||
![]() |
8aab7d5504 | ||
![]() |
c52f73269e | ||
![]() |
fa2ad54d90 | ||
![]() |
fa9af6a021 | ||
![]() |
a3605921c9 | ||
![]() |
e9269a1d33 | ||
![]() |
39807abc7d | ||
![]() |
08ae05cc76 | ||
![]() |
2188603a49 | ||
![]() |
fbe2370df7 | ||
![]() |
8fb1c6535d | ||
![]() |
6534dff4bc | ||
![]() |
44f2897919 | ||
![]() |
30b7e36f10 | ||
![]() |
2f6ad8ea4a | ||
![]() |
78afd566ec | ||
![]() |
02cc679692 | ||
![]() |
1064588c00 | ||
![]() |
54050f10b7 | ||
![]() |
f0cf620854 | ||
![]() |
80f34620c8 | ||
![]() |
7030000348 | ||
![]() |
fc7f1ab42f | ||
![]() |
89a6bc4354 | ||
![]() |
849121a124 | ||
![]() |
ba6ce28d3c | ||
![]() |
4b8447bc82 | ||
![]() |
6698b3a1dc | ||
![]() |
352ef0d009 | ||
![]() |
bb73ecc1f4 | ||
![]() |
7ea8827e69 | ||
![]() |
8fa48a9781 | ||
![]() |
694c768666 | ||
![]() |
51eb4770a7 | ||
![]() |
274a507bc4 | ||
![]() |
da8339066b | ||
![]() |
ee3ee5b165 | ||
![]() |
e86bffdf89 | ||
![]() |
931161b007 | ||
![]() |
2d30ae2bd9 | ||
![]() |
1ad60881cb | ||
![]() |
18cd389c77 | ||
![]() |
26ea97cb44 | ||
![]() |
d7f6db5efd | ||
![]() |
99b25efb67 | ||
![]() |
8699e69ae5 | ||
![]() |
f928818bf1 | ||
![]() |
35e26629af | ||
![]() |
6e7f49591f | ||
![]() |
7fea432102 | ||
![]() |
b76cddcf9f | ||
![]() |
9b2faf207d | ||
![]() |
9e59f07401 | ||
![]() |
928faeba0d | ||
![]() |
521a44b953 | ||
![]() |
fb60479578 | ||
![]() |
29b67505a7 | ||
![]() |
6f178a8a23 | ||
![]() |
b29c295adc | ||
![]() |
8b0f9d4317 | ||
![]() |
e1ba2a8ca2 | ||
![]() |
eb852cec43 | ||
![]() |
0bed5727cb | ||
![]() |
a749ecceed | ||
![]() |
b5b934b8a1 | ||
![]() |
6b09fe2377 | ||
![]() |
cbb4ff2fd9 | ||
![]() |
eb642e8a06 | ||
![]() |
b97d8e163d | ||
![]() |
7c0d2832cd | ||
![]() |
205cfae1a4 | ||
![]() |
f861a2b72c | ||
![]() |
5843e63878 | ||
![]() |
961f8afe53 | ||
![]() |
37769b94cd | ||
![]() |
f5c0c207ec | ||
![]() |
b4344a8de2 | ||
![]() |
e02c200775 | ||
![]() |
8f4435019b | ||
![]() |
ec55f716e1 | ||
![]() |
012f6b660c | ||
![]() |
9de136789c | ||
![]() |
84a8c1312f | ||
![]() |
21f9ad3994 | ||
![]() |
a9e77dc0db | ||
![]() |
626eb77060 | ||
![]() |
7674f6b5aa | ||
![]() |
16c72c491d | ||
![]() |
99e1245c9b | ||
![]() |
f42b137c1b | ||
![]() |
4e852911aa | ||
![]() |
30ab068bfe | ||
![]() |
6499ad6cdb | ||
![]() |
6f99b1d69b | ||
![]() |
42c4ed85a1 | ||
![]() |
3e3697dc7a | ||
![]() |
ff1ab1da37 | ||
![]() |
3da77726d0 | ||
![]() |
879cdcc0a4 | ||
![]() |
9b8a35dbb3 | ||
![]() |
7c7f18b501 | ||
![]() |
6f0a9910ea | ||
![]() |
6e8c971038 | ||
![]() |
09131d8647 | ||
![]() |
930fa18224 | ||
![]() |
9c9c115d1a | ||
![]() |
b6e9643586 | ||
![]() |
83f2acddf8 | ||
![]() |
35f9cc55f1 | ||
![]() |
4483025856 | ||
![]() |
61e4be4456 | ||
![]() |
f11f4510a2 | ||
![]() |
b3c3be0483 | ||
![]() |
7c3df46570 | ||
![]() |
b8793760a1 | ||
![]() |
6264f9c67b | ||
![]() |
2a74deb84e | ||
![]() |
9d1ff37a79 | ||
![]() |
2f99164781 | ||
![]() |
80ef32f09d | ||
![]() |
63be0e2e1a | ||
![]() |
74c4553bb0 | ||
![]() |
e240707b32 | ||
![]() |
7c867852a9 | ||
![]() |
2d149dc746 | ||
![]() |
7edcddd3e4 | ||
![]() |
71f658b560 | ||
![]() |
9886db5d6d | ||
![]() |
c236cd070c | ||
![]() |
9f1a830d32 | ||
![]() |
1e69ce9111 | ||
![]() |
389297155d | ||
![]() |
c341b86520 | ||
![]() |
88eef379b2 | ||
![]() |
34767d4058 | ||
![]() |
12c3d54a63 | ||
![]() |
33a185dade | ||
![]() |
c1c5776d85 | ||
![]() |
eda642554d | ||
![]() |
51f5ce013f | ||
![]() |
44450f9d7d | ||
![]() |
c34e280fc2 | ||
![]() |
aef266b940 | ||
![]() |
f873219d25 | ||
![]() |
7de5646d6b | ||
![]() |
1e89f3ec9a | ||
![]() |
d8d9449e2b | ||
![]() |
850d9a0254 | ||
![]() |
c422bcf1e2 | ||
![]() |
27b7fb6f91 | ||
![]() |
302dbc424b | ||
![]() |
a7922690c4 | ||
![]() |
5541de2bcb | ||
![]() |
4c14184111 | ||
![]() |
d78f63e4d0 | ||
![]() |
f38d50b928 | ||
![]() |
5c5b832d01 | ||
![]() |
2b4c5178f4 | ||
![]() |
59588f960d | ||
![]() |
d3b335f53f | ||
![]() |
c3037bae39 | ||
![]() |
9b1ab34352 | ||
![]() |
221a8597da | ||
![]() |
45022752a0 | ||
![]() |
aa342eb476 | ||
![]() |
32b26b8270 | ||
![]() |
e07c29caad | ||
![]() |
b487c12ab1 | ||
![]() |
60293648dc | ||
![]() |
e7994b3da1 | ||
![]() |
b88bf74e13 | ||
![]() |
8355727eb1 | ||
![]() |
c7290908cc | ||
![]() |
1307cd4b10 | ||
![]() |
c0b2193718 | ||
![]() |
f9bb7e404e | ||
![]() |
bbb8a1bacc | ||
![]() |
0aaa4fa79b | ||
![]() |
4ed81fb03f | ||
![]() |
7d13c2d854 | ||
![]() |
cadbb623c7 | ||
![]() |
4d959fb91c | ||
![]() |
1fb3d8d601 | ||
![]() |
dd4334e3ba | ||
![]() |
5eee47d1e4 | ||
![]() |
54def1ae0e | ||
![]() |
6a36fc75cf | ||
![]() |
bf69d4e0a8 | ||
![]() |
fe248a2ebd | ||
![]() |
49ad9a8bd5 | ||
![]() |
fa75b477e9 | ||
![]() |
3c1d93f503 | ||
![]() |
21fabd3afa | ||
![]() |
0ec4652b52 | ||
![]() |
e901dc4ec4 | ||
![]() |
9d02436a72 | ||
![]() |
9fb7542a6f | ||
![]() |
ddf37a847d | ||
![]() |
024ec2b153 | ||
![]() |
f8b56c460e | ||
![]() |
42277955fa | ||
![]() |
950c332e36 | ||
![]() |
44d6f0bc2b | ||
![]() |
9bff88ad3e | ||
![]() |
8de23b9559 | ||
![]() |
fbba0d9a21 | ||
![]() |
5beb415ada | ||
![]() |
187024367a | ||
![]() |
e6262de5ab | ||
![]() |
c96bb45940 | ||
![]() |
50796a6a77 | ||
![]() |
0fb0e132b6 | ||
![]() |
494a991d10 | ||
![]() |
c32654db18 | ||
![]() |
f4e7ccfcfc | ||
![]() |
4a4cbe011a | ||
![]() |
f68111c59f | ||
![]() |
1d845623a8 | ||
![]() |
a93121a88d | ||
![]() |
4ea1d88826 | ||
![]() |
a87b6fee89 | ||
![]() |
9baf5ad404 | ||
![]() |
5fd7306446 | ||
![]() |
3a8828325a | ||
![]() |
57bf59f6bd | ||
![]() |
f0d81d077f | ||
![]() |
bb5aefb9e4 | ||
![]() |
6a1739e883 | ||
![]() |
ae306893ff | ||
![]() |
5fd17d092b | ||
![]() |
fad1d7bd1f | ||
![]() |
dcf7520d2a | ||
![]() |
998b33c207 | ||
![]() |
7b3e7b7aea | ||
![]() |
09a86d2ed2 | ||
![]() |
285f7ec696 | ||
![]() |
595508bf7d | ||
![]() |
2074c7fcee | ||
![]() |
759d8a3f90 | ||
![]() |
f2fa583101 | ||
![]() |
18feb4bb81 | ||
![]() |
b49a60fa3e | ||
![]() |
fa81a83893 | ||
![]() |
942bf2ef78 | ||
![]() |
a9d4b1afe4 | ||
![]() |
cdd8ba78e7 | ||
![]() |
254d4c6534 | ||
![]() |
33a0db3935 | ||
![]() |
514f83cc96 | ||
![]() |
4950bda406 | ||
![]() |
9e9be6055d | ||
![]() |
c9ccc79789 | ||
![]() |
3fab596518 | ||
![]() |
3378b8d7ce | ||
![]() |
881079ccc1 | ||
![]() |
cf1cbc6d75 | ||
![]() |
e418491f19 | ||
![]() |
a4f75ca249 | ||
![]() |
6ba2d0be31 | ||
![]() |
fb2a671e86 | ||
![]() |
8cb62341ef | ||
![]() |
074378bef6 | ||
![]() |
a772832917 | ||
![]() |
cf467b8593 | ||
![]() |
b4a3470cb9 | ||
![]() |
e44d86479e | ||
![]() |
42345d9a06 | ||
![]() |
40fd7cf852 | ||
![]() |
870350b961 | ||
![]() |
198a6b2e8f | ||
![]() |
0479fc6f54 | ||
![]() |
be6e1e5e15 | ||
![]() |
9ce44845fe | ||
![]() |
1463f05d46 | ||
![]() |
49a9923b5c | ||
![]() |
23844c0f1a | ||
![]() |
82efa0893f | ||
![]() |
9e93d1fd7e | ||
![]() |
0a424f53b1 | ||
![]() |
efc44d83bb | ||
![]() |
9b274a0bc4 | ||
![]() |
aeca2842fe | ||
![]() |
d44d07ffcf | ||
![]() |
bc683ce6ee | ||
![]() |
c6abe1d1bb | ||
![]() |
6a95abb831 | ||
![]() |
6d74a6aa19 | ||
![]() |
83c3275054 | ||
![]() |
b3eb0301ae | ||
![]() |
f00dfd32d4 | ||
![]() |
8ec436423f | ||
![]() |
5f2ae37ee5 | ||
![]() |
514363f1c5 | ||
![]() |
f84f6aa713 | ||
![]() |
b5f15b6d67 | ||
![]() |
c8972a2234 | ||
![]() |
1892c8fa62 | ||
![]() |
589633bc23 | ||
![]() |
458162c3f5 | ||
![]() |
583eb1a80e | ||
![]() |
1480b77461 | ||
![]() |
908a7c6991 | ||
![]() |
53b991fb54 | ||
![]() |
35187a4b52 | ||
![]() |
621326f4e4 | ||
![]() |
a6643d8fb3 | ||
![]() |
9239ace1c8 | ||
![]() |
422bcecec1 | ||
![]() |
6f02550ac3 | ||
![]() |
1aa996d5f0 | ||
![]() |
a340646e1e | ||
![]() |
db043b26da | ||
![]() |
8767599ad4 | ||
![]() |
6d5c000e1f | ||
![]() |
2750535928 | ||
![]() |
1e31e2944b | ||
![]() |
8bcc4f4c82 | ||
![]() |
bfc3080292 | ||
![]() |
9bff86e7aa | ||
![]() |
0689a6ed62 | ||
![]() |
61f2251336 | ||
![]() |
62a0932deb | ||
![]() |
3389ee4b80 | ||
![]() |
cc6e2ef3f7 | ||
![]() |
1a1c95af12 | ||
![]() |
658299ee21 | ||
![]() |
5b8ca8d0ed | ||
![]() |
d91528648f | ||
![]() |
8b88272bc0 | ||
![]() |
8ab59bee47 | ||
![]() |
18c814d3dc | ||
![]() |
4f0928d93b | ||
![]() |
6c7865a247 | ||
![]() |
7cf63d1985 | ||
![]() |
0b02b43b11 | ||
![]() |
b25a0e2272 | ||
![]() |
6d78c961d9 | ||
![]() |
e370248c9e | ||
![]() |
31c2d22912 | ||
![]() |
5eb25b2d4a | ||
![]() |
6737c51fca | ||
![]() |
d23c9f715e | ||
![]() |
03ccb529e4 | ||
![]() |
505e09242d | ||
![]() |
d6b4f1c95d | ||
![]() |
67c0af4c57 | ||
![]() |
cba0cf0609 | ||
![]() |
b957017799 | ||
![]() |
ebe71a1a38 | ||
![]() |
6feb9d4b4e | ||
![]() |
06d6155862 | ||
![]() |
f13bdd0da4 | ||
![]() |
3489ea30dd | ||
![]() |
d218ac85f7 | ||
![]() |
5129c7521b | ||
![]() |
eb19c7af32 | ||
![]() |
6b65b21ee0 | ||
![]() |
4eda081574 | ||
![]() |
234c4c1958 | ||
![]() |
ad3c4d24b8 | ||
![]() |
ee37b32ca1 | ||
![]() |
49721a541a | ||
![]() |
c18d96e2f5 | ||
![]() |
3efb009e82 | ||
![]() |
b20f46e8b9 | ||
![]() |
4f0ece1bb4 | ||
![]() |
2c316c5820 | ||
![]() |
9d10d8f55e | ||
![]() |
0fcac987df | ||
![]() |
ffcc2254ce | ||
![]() |
b01eac3ba5 | ||
![]() |
a3341c4330 | ||
![]() |
a4fac730d4 | ||
![]() |
ca07975ead | ||
![]() |
5a1a41beb1 | ||
![]() |
20a3a061a1 | ||
![]() |
7b78f6db17 | ||
![]() |
5816a24577 | ||
![]() |
0e4f44b775 | ||
![]() |
0105332476 | ||
![]() |
4aca9cd66b | ||
![]() |
a4234bf80e | ||
![]() |
f42f698dbc | ||
![]() |
cd45c5d886 | ||
![]() |
2af6ee7584 | ||
![]() |
3b437c9b84 | ||
![]() |
af8ecdd48d | ||
![]() |
16d9ccd423 | ||
![]() |
e1d223f726 | ||
![]() |
a4904a3f2d | ||
![]() |
dff7b30405 | ||
![]() |
a06cd770a4 | ||
![]() |
56c4121eb2 | ||
![]() |
f519b20495 | ||
![]() |
32da8c52f7 | ||
![]() |
c6994731b1 | ||
![]() |
2eb1041f4b | ||
![]() |
4ad5eb5a82 | ||
![]() |
ea38639395 | ||
![]() |
bf0d2e9bd2 | ||
![]() |
bb3c2175bc | ||
![]() |
6fafafbed0 | ||
![]() |
8f73c53d26 | ||
![]() |
5a09847596 | ||
![]() |
cf63175232 | ||
![]() |
88428fc772 | ||
![]() |
505dfcbcd9 | ||
![]() |
7cbcb21e80 | ||
![]() |
d4dbd76a0a | ||
![]() |
1d9343df7f | ||
![]() |
c7ca88e666 | ||
![]() |
efbb94a1b1 | ||
![]() |
eee6e8a2c3 | ||
![]() |
a26cdef427 | ||
![]() |
a5013cddd5 | ||
![]() |
844515787b | ||
![]() |
d5476a1da1 | ||
![]() |
12ae70630f | ||
![]() |
954a47d9ef | ||
![]() |
6ed847f49e | ||
![]() |
ea50bbeb11 | ||
![]() |
aefadd6684 | ||
![]() |
4096a8931a | ||
![]() |
d2bd0e8ca2 | ||
![]() |
e119675100 | ||
![]() |
5ff2608794 | ||
![]() |
96d1c9ab91 | ||
![]() |
60268e97d4 | ||
![]() |
b51bb668c6 | ||
![]() |
87e5b024c1 | ||
![]() |
54f3bb8ddf | ||
![]() |
fa291c20e5 | ||
![]() |
dd97d5bc7e | ||
![]() |
b3fccc0de6 | ||
![]() |
9fe306f056 | ||
![]() |
1b66278a68 | ||
![]() |
76015740f8 | ||
![]() |
816edb66c7 | ||
![]() |
82c688e3be | ||
![]() |
46d6241f58 | ||
![]() |
b5083ce973 | ||
![]() |
1663756983 | ||
![]() |
f344314762 | ||
![]() |
7f4d178781 | ||
![]() |
ba629fbddb | ||
![]() |
157c776019 | ||
![]() |
70aacfce98 | ||
![]() |
8625a36d1d | ||
![]() |
002f5b5ee6 | ||
![]() |
b058b2574f | ||
![]() |
170e6bdcab | ||
![]() |
075a0ad780 | ||
![]() |
e7c2e86c93 | ||
![]() |
3ca1f07cc4 | ||
![]() |
762c752918 | ||
![]() |
06a2de4d1c | ||
![]() |
d4f47bfc6b | ||
![]() |
271a4ba7c8 | ||
![]() |
528ca49368 | ||
![]() |
5d8c90ae0d | ||
![]() |
f872dc8948 | ||
![]() |
ec520b8cf5 | ||
![]() |
ff8b96a19f | ||
![]() |
f6b55c7eb9 | ||
![]() |
3aae280de5 | ||
![]() |
6c1f9e39c4 | ||
![]() |
a957db7c27 | ||
![]() |
3a670e74f7 | ||
![]() |
a114ecfb73 | ||
![]() |
626935ee14 | ||
![]() |
3f2975e93f | ||
![]() |
38bf06e179 | ||
![]() |
12fc458abb | ||
![]() |
0ed7348d2d | ||
![]() |
67e7554702 | ||
![]() |
74141c39ea | ||
![]() |
cb09207cd7 | ||
![]() |
894cc7cc4d | ||
![]() |
36192ebc3a | ||
![]() |
26663756a5 | ||
![]() |
167e766811 | ||
![]() |
08304ca5f3 | ||
![]() |
323c459442 | ||
![]() |
89c9288706 | ||
![]() |
480d645650 | ||
![]() |
553091e95e | ||
![]() |
dacc4c230d | ||
![]() |
cb07e64b47 | ||
![]() |
8dee5851d2 | ||
![]() |
c14380247b | ||
![]() |
c6ac8780ca | ||
![]() |
8f3f8fa35f | ||
![]() |
1cedacc395 | ||
![]() |
19a39a3647 | ||
![]() |
5c2f19de88 | ||
![]() |
4ccd30865b | ||
![]() |
7ad13c8897 | ||
![]() |
4813b5c882 | ||
![]() |
a787c6a31e | ||
![]() |
f2e4bcea19 | ||
![]() |
cd2313d2ca | ||
![]() |
79b984d612 | ||
![]() |
bf003d643c | ||
![]() |
04fa699498 | ||
![]() |
4020c987b5 | ||
![]() |
2818f74634 | ||
![]() |
a026820483 | ||
![]() |
33fa8df73e | ||
![]() |
2ed70ef241 | ||
![]() |
04dfa45db0 | ||
![]() |
8d82ef8e36 | ||
![]() |
43f93c74da | ||
![]() |
1e104ba40b | ||
![]() |
056d26f13c | ||
![]() |
3e4a077862 | ||
![]() |
a44adf2e6f | ||
![]() |
7c488f1e54 | ||
![]() |
bea389eed7 | ||
![]() |
e96f2f06fb | ||
![]() |
9a897d5e12 | ||
![]() |
b35a44a0e0 | ||
![]() |
8aee79085a | ||
![]() |
d7ca168b77 | ||
![]() |
638b88c61c | ||
![]() |
62845fe4a7 | ||
![]() |
c93b4cf61a | ||
![]() |
55de21477c | ||
![]() |
dcef86a30d | ||
![]() |
0a7b4d18dc | ||
![]() |
bd8c723e08 | ||
![]() |
cd7d7cd35c | ||
![]() |
33cbebc727 | ||
![]() |
6da37691ff | ||
![]() |
ae0f27c42f | ||
![]() |
660cbc136f | ||
![]() |
52143155e7 | ||
![]() |
a29ba51bdb | ||
![]() |
051a503047 | ||
![]() |
8121d147a6 | ||
![]() |
913d3d4ac6 | ||
![]() |
236f33537b | ||
![]() |
9f4b2ad05a | ||
![]() |
f290199606 | ||
![]() |
9692d637ca | ||
![]() |
904265bca7 | ||
![]() |
1ab8deff3d | ||
![]() |
4ab31e2d4e | ||
![]() |
d7e36513b5 | ||
![]() |
d07378e87b | ||
![]() |
e235a04dae | ||
![]() |
be32968ed4 | ||
![]() |
31c660557d | ||
![]() |
414fe53261 | ||
![]() |
1d10c81ff3 | ||
![]() |
1e55d4b613 | ||
![]() |
f9cd0f37f7 | ||
![]() |
39ebc103df | ||
![]() |
52724c5c22 | ||
![]() |
0abe57edaa | ||
![]() |
8d95fb3b31 | ||
![]() |
69e241d2e6 | ||
![]() |
64e1735647 | ||
![]() |
9c538d1e22 | ||
![]() |
3c60bff7dc | ||
![]() |
61d2c9335f | ||
![]() |
f4ed9edec6 | ||
![]() |
5eea5858ea | ||
![]() |
a05785529f | ||
![]() |
a407a3c98d | ||
![]() |
986095482f | ||
![]() |
5ca0441771 | ||
![]() |
79fe8650f8 | ||
![]() |
93418f587c | ||
![]() |
9ed8419b5d | ||
![]() |
b5721604b9 | ||
![]() |
b7d9ad1c7d | ||
![]() |
5e04347f13 | ||
![]() |
1cc8a170e6 | ||
![]() |
88455702bb | ||
![]() |
471b05ff4b | ||
![]() |
95ffa20bd5 | ||
![]() |
5424fa0a00 | ||
![]() |
7152c86591 | ||
![]() |
b9e17c6cc6 | ||
![]() |
7751964db4 | ||
![]() |
2f180c96c8 | ||
![]() |
74d6019f81 | ||
![]() |
b84096097c | ||
![]() |
b9d819e0e5 | ||
![]() |
30e50d261d | ||
![]() |
3b2ff38f02 | ||
![]() |
3ed4859db9 | ||
![]() |
380fb6176b | ||
![]() |
fefa2a9dd6 | ||
![]() |
53d2347c10 | ||
![]() |
b2af1084f9 | ||
![]() |
cf005feace | ||
![]() |
7a9a4db8d7 | ||
![]() |
934e81db43 | ||
![]() |
8b3a43258d | ||
![]() |
b7bc9607a2 | ||
![]() |
c2eb72fce4 | ||
![]() |
1d694450ef | ||
![]() |
98c56bce4b | ||
![]() |
ec396513a2 | ||
![]() |
0b61b62334 | ||
![]() |
dfa180ba64 | ||
![]() |
db44ed845d | ||
![]() |
4a562b5085 | ||
![]() |
03c70e18df | ||
![]() |
df5cdf7de4 | ||
![]() |
1860db4632 | ||
![]() |
09d25f322a | ||
![]() |
33d895bc7d | ||
![]() |
02ca1f2889 | ||
![]() |
e8335b1ed7 | ||
![]() |
ec96e54f87 | ||
![]() |
519a416837 | ||
![]() |
d56a3ac652 | ||
![]() |
d13beec3e1 | ||
![]() |
5d0de138f6 | ||
![]() |
06edb2e36b | ||
![]() |
23ade8180a | ||
![]() |
314f658d92 | ||
![]() |
48cbe22609 | ||
![]() |
f8a15c8228 | ||
![]() |
17f6ded7b0 | ||
![]() |
691cb378a0 | ||
![]() |
2876e5d0cd | ||
![]() |
2601217209 | ||
![]() |
a7be9e6643 | ||
![]() |
30ea27d4a5 | ||
![]() |
2a66c03d73 | ||
![]() |
6b34c38d21 | ||
![]() |
0871bf13a4 | ||
![]() |
4c44d2f4d9 | ||
![]() |
833a8be2d1 | ||
![]() |
f8113ae80b | ||
![]() |
feff5355c8 | ||
![]() |
6fbee5c2e3 | ||
![]() |
8200c234dd | ||
![]() |
dfd86d56ec | ||
![]() |
93162f6b65 | ||
![]() |
93ea88f3de | ||
![]() |
ca48b07858 | ||
![]() |
795e01512a | ||
![]() |
36857b4b20 | ||
![]() |
8432b6a790 | ||
![]() |
e02a6f2f19 | ||
![]() |
6b45b0f522 | ||
![]() |
c35ec1f12b | ||
![]() |
bb7e1d4723 | ||
![]() |
2305cb0131 | ||
![]() |
253293c986 | ||
![]() |
1040fe50ec | ||
![]() |
6a012498a5 | ||
![]() |
74c2060c49 | ||
![]() |
177fff3ff0 | ||
![]() |
e7fadcda7b | ||
![]() |
91c53e9c52 | ||
![]() |
bd1c66984f | ||
![]() |
704777444c | ||
![]() |
c28a6a867d | ||
![]() |
4bfc96c02b | ||
![]() |
faac51d219 | ||
![]() |
d9cd62bf65 | ||
![]() |
6007629293 | ||
![]() |
426e9846d9 | ||
![]() |
935db1308f | ||
![]() |
597540b611 | ||
![]() |
e0b030c892 | ||
![]() |
da9b3dc68b | ||
![]() |
23b79b2f39 | ||
![]() |
b9a0d553ab | ||
![]() |
c4f0d9d2fa | ||
![]() |
78338f161f | ||
![]() |
aaafdee56f | ||
![]() |
7068986c14 | ||
![]() |
32ee31b8c7 | ||
![]() |
50c12d4487 | ||
![]() |
2427b77363 | ||
![]() |
fa9613a879 | ||
![]() |
145e02769c | ||
![]() |
c151696357 | ||
![]() |
cbcd1929dd | ||
![]() |
7a9836064d | ||
![]() |
3155c1cd4f | ||
![]() |
28c38e92d4 | ||
![]() |
9c3b9eee2a | ||
![]() |
def50b255d | ||
![]() |
aa7694e81c | ||
![]() |
a722912e05 | ||
![]() |
a09213bce8 | ||
![]() |
0abaaa0a06 | ||
![]() |
363b88407c | ||
![]() |
4a4458ec5b | ||
![]() |
b3379e1921 | ||
![]() |
09e5fbb258 | ||
![]() |
b758dc202f | ||
![]() |
c5f75bc135 | ||
![]() |
a904df5bc2 | ||
![]() |
1978e94aaa | ||
![]() |
28dbf6e3dc | ||
![]() |
ef989160af | ||
![]() |
4071eb76c7 | ||
![]() |
ac723161c1 | ||
![]() |
94884d33db | ||
![]() |
64994277b1 | ||
![]() |
8abf822d92 | ||
![]() |
6e6f10c085 | ||
![]() |
1c0768dd78 | ||
![]() |
c888502671 | ||
![]() |
58af3545f4 | ||
![]() |
d669dd45cf | ||
![]() |
05a5b8cdf0 | ||
![]() |
33b6d0a45f | ||
![]() |
fba11d8016 | ||
![]() |
314834b4eb | ||
![]() |
46a8325556 | ||
![]() |
86622cd29d | ||
![]() |
c91a1d0fce | ||
![]() |
778a2891ce | ||
![]() |
560c719b0f | ||
![]() |
d5ab86edbf | ||
![]() |
6aeb7f36f6 | ||
![]() |
f6308368b0 | ||
![]() |
c0e8f14745 | ||
![]() |
0488012c77 | ||
![]() |
f247183e11 | ||
![]() |
c662b94d06 | ||
![]() |
ee4bf165b5 | ||
![]() |
92ac396d19 | ||
![]() |
03366038ce | ||
![]() |
0b91aa9202 | ||
![]() |
ffc4fa1c2a | ||
![]() |
15e03957a9 | ||
![]() |
0be881bca6 | ||
![]() |
e88b321741 | ||
![]() |
0c4cb27fe9 | ||
![]() |
1639163c2e | ||
![]() |
f043404cd9 | ||
![]() |
018651ff1d | ||
![]() |
704d7a037c | ||
![]() |
7336178e03 | ||
![]() |
1c16fb8e42 | ||
![]() |
3ab2cd3fb7 | ||
![]() |
5057343b6a | ||
![]() |
6c3e85fd5e | ||
![]() |
f046456445 | ||
![]() |
e81a08916a | ||
![]() |
85d2e3d006 | ||
![]() |
936b0b32ed | ||
![]() |
0d511c697c | ||
![]() |
5bfe034b4d | ||
![]() |
cf786b3b04 | ||
![]() |
0f9f090db2 | ||
![]() |
302eea7418 | ||
![]() |
b5e1f7e03e | ||
![]() |
02397a8d2d | ||
![]() |
ea9437eab2 | ||
![]() |
aaea30bee0 | ||
![]() |
9c869fa701 | ||
![]() |
5106548f2c | ||
![]() |
506d485c0d | ||
![]() |
da190ec96f | ||
![]() |
9567929484 | ||
![]() |
dc16494332 | ||
![]() |
933f422588 | ||
![]() |
663d0691a7 | ||
![]() |
97a0b9272e | ||
![]() |
3d49000c75 | ||
![]() |
68d1a3c0a2 | ||
![]() |
b06de7a687 | ||
![]() |
963ea6141c | ||
![]() |
7232d36494 | ||
![]() |
12eb071e8a | ||
![]() |
86be626c69 | ||
![]() |
ec20e41836 | ||
![]() |
b3564b6cff | ||
![]() |
acbee815be | ||
![]() |
5a1aeff85c | ||
![]() |
4463e4c42b | ||
![]() |
4103ef71c9 | ||
![]() |
dccaa2dd2d | ||
![]() |
5ac6096e08 | ||
![]() |
4734a82f99 | ||
![]() |
c6c2309dee | ||
![]() |
5e1bbd8bff | ||
![]() |
4761207097 | ||
![]() |
b4a6ca63b3 | ||
![]() |
a5b320180a | ||
![]() |
bcc767136c | ||
![]() |
d3257d96d0 | ||
![]() |
89df6a82b0 | ||
![]() |
e725ba403b | ||
![]() |
578fece13e | ||
![]() |
0eeb6b5fd5 | ||
![]() |
ad3f7f041f | ||
![]() |
dce9bfd359 | ||
![]() |
8f96ccc835 | ||
![]() |
a48dd05035 | ||
![]() |
eb90958341 | ||
![]() |
31ed6a48cb | ||
![]() |
dfa80f0787 | ||
![]() |
73acfa6a8e | ||
![]() |
efad20cdff | ||
![]() |
11d68cef54 | ||
![]() |
9c28e60475 | ||
![]() |
9ee79b87ee | ||
![]() |
600aedc9a1 | ||
![]() |
5b5efb5aaa | ||
![]() |
391b3ed1e7 | ||
![]() |
24277259ad | ||
![]() |
65261de7cc | ||
![]() |
beb92a7f9c | ||
![]() |
f1b059c75d | ||
![]() |
d4970f81aa | ||
![]() |
29219afb7f | ||
![]() |
7fbf15edc9 | ||
![]() |
c8d3fa6768 | ||
![]() |
ea5cf3d854 | ||
![]() |
4a833fb489 | ||
![]() |
02aa823d25 | ||
![]() |
92034aeecc | ||
![]() |
9f2232fad1 | ||
![]() |
2be2d54a5c | ||
![]() |
ed99686cc1 | ||
![]() |
a6c1f1e485 | ||
![]() |
a219445751 | ||
![]() |
b65b5aacb6 | ||
![]() |
bcead72265 | ||
![]() |
35b9564ed4 | ||
![]() |
aba01d4361 | ||
![]() |
4398af51c8 | ||
![]() |
83f4f4cc96 | ||
![]() |
1800e6fb8e | ||
![]() |
43b83c855f | ||
![]() |
20e2de200f | ||
![]() |
ed4ebe1222 | ||
![]() |
4e4446cef4 | ||
![]() |
e2ff0b265d | ||
![]() |
6d48fc183a | ||
![]() |
ea8392a4a1 | ||
![]() |
bcd296822d | ||
![]() |
6ee97f341d | ||
![]() |
aa2ab74ee9 | ||
![]() |
49b2ab9889 | ||
![]() |
2549e2cc0f | ||
![]() |
b15fa81a44 | ||
![]() |
09f6246d1b | ||
![]() |
96ff389fd1 | ||
![]() |
b55f1df297 | ||
![]() |
d88f7b8600 | ||
![]() |
df2a94bb5b | ||
![]() |
c4ac492c6e | ||
![]() |
fcd4d3e2df | ||
![]() |
42d6bd3839 | ||
![]() |
d6b48003b6 | ||
![]() |
ba8f69d956 | ||
![]() |
f22bb72d18 | ||
![]() |
f7a0a9fa41 | ||
![]() |
3795d653c5 | ||
![]() |
8ee014b855 | ||
![]() |
1ab5bdf85f | ||
![]() |
5283e1a39f | ||
![]() |
17c56208ee | ||
![]() |
8474d9fefe | ||
![]() |
fd9f002e9f | ||
![]() |
26268357a0 | ||
![]() |
82b463b22f | ||
![]() |
7ae397a211 | ||
![]() |
ea4ad681e4 | ||
![]() |
a150f9d5ad | ||
![]() |
afb7fe0d40 | ||
![]() |
8bf42b9d3e | ||
![]() |
ba00707d89 | ||
![]() |
2121b943a3 | ||
![]() |
ef06d2c06e | ||
![]() |
cc1fac5776 | ||
![]() |
6cb3430c60 | ||
![]() |
4cea90f773 | ||
![]() |
4da5f6188d | ||
![]() |
e7f8b9ad92 | ||
![]() |
473a28c5f2 | ||
![]() |
d765936be3 | ||
![]() |
0db643d9d1 | ||
![]() |
af29159e2f | ||
![]() |
65c38d8e31 | ||
![]() |
adb7aa237b | ||
![]() |
577f86b83a | ||
![]() |
01169e9184 | ||
![]() |
dde037291a | ||
![]() |
6971a189f9 | ||
![]() |
c860686138 | ||
![]() |
2eb507863f | ||
![]() |
54ee5c6998 | ||
![]() |
63df2474a9 | ||
![]() |
93f12fb7c6 | ||
![]() |
b7a995ac53 | ||
![]() |
267a80e70c | ||
![]() |
078be3b8df | ||
![]() |
7b6c967c3a | ||
![]() |
a405ccd044 | ||
![]() |
f6c55ebf05 | ||
![]() |
6b3b4cce4b | ||
![]() |
8887c979b4 | ||
![]() |
665541409a | ||
![]() |
195919b5fb | ||
![]() |
31479056ed | ||
![]() |
9f0976d94a | ||
![]() |
a049d2b7db | ||
![]() |
6959017d55 | ||
![]() |
4ff5a04a72 | ||
![]() |
ea0c4a7263 | ||
![]() |
9633f03ddc | ||
![]() |
4c0d8ce87c | ||
![]() |
d92728e533 | ||
![]() |
799962ef0e | ||
![]() |
631f817f11 | ||
![]() |
52f7bdeb5d | ||
![]() |
51db140aed | ||
![]() |
1ad12d5945 | ||
![]() |
9f5d94046d | ||
![]() |
de1e06c39b | ||
![]() |
abbabc11d2 | ||
![]() |
62be82fd3c | ||
![]() |
f0fd5a639a | ||
![]() |
dea00fac3f | ||
![]() |
c30f17f592 | ||
![]() |
e9e95f45d8 | ||
![]() |
e8aa3e6d34 | ||
![]() |
3646884d79 | ||
![]() |
5747c6b1a8 | ||
![]() |
43a5c7ddc8 | ||
![]() |
d9d74107fe | ||
![]() |
373cca9857 | ||
![]() |
284b3f444d | ||
![]() |
dfb088e524 | ||
![]() |
5546f1d73d | ||
![]() |
13fc871806 | ||
![]() |
0f9fd78656 | ||
![]() |
4f318c0be3 | ||
![]() |
5eb1d0a28e | ||
![]() |
66c03713b7 | ||
![]() |
d51070c99b | ||
![]() |
50d050e63e | ||
![]() |
89bf426163 | ||
![]() |
377548e3a1 | ||
![]() |
543c6929e6 | ||
![]() |
42ae572948 | ||
![]() |
c3f8b7e200 | ||
![]() |
3a207e2571 | ||
![]() |
6bfd39f094 | ||
![]() |
002ca9611d | ||
![]() |
46ee3d2b26 | ||
![]() |
eb901bcf3a | ||
![]() |
930b4a2c81 | ||
![]() |
22d1b8e1cd | ||
![]() |
e7e42dc318 | ||
![]() |
9aa288ed44 | ||
![]() |
5aacb6e1b8 | ||
![]() |
1428ce4084 | ||
![]() |
dba07ac90d | ||
![]() |
264df97069 | ||
![]() |
c3f493394a | ||
![]() |
7e3e82746f | ||
![]() |
33724240d7 | ||
![]() |
998a4eab9e | ||
![]() |
9985262a53 |
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
description: Report an issue with Home Assistant Core.
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
18
.github/workflows/builder.yml
vendored
18
.github/workflows/builder.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
uses: sigstore/cosign-installer@v3.8.2
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
@@ -457,12 +457,12 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
140
.github/workflows/ci.yaml
vendored
140
.github/workflows/ci.yaml
vendored
@@ -37,10 +37,10 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 12
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -259,7 +259,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Install pre-commit dependencies
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -306,7 +306,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Run ruff-format
|
||||
run: |
|
||||
@@ -334,7 +334,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -346,7 +346,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
@@ -355,7 +355,7 @@ jobs:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Run ruff
|
||||
run: |
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
|
||||
- name: Register yamllint problem matcher
|
||||
@@ -484,7 +484,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -501,7 +501,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -509,10 +509,10 @@ jobs:
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-uv-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
|
||||
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
|
||||
env.HA_SHORT_VERSION }}-
|
||||
- name: Install additional OS dependencies
|
||||
@@ -587,7 +587,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -598,7 +598,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run hassfest
|
||||
run: |
|
||||
@@ -620,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -631,7 +631,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run gen_requirements_all.py
|
||||
run: |
|
||||
@@ -653,7 +653,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.5.0
|
||||
uses: actions/dependency-review-action@v4.7.1
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -677,7 +677,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Extract license data
|
||||
run: |
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -731,7 +731,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register pylint problem matcher
|
||||
run: |
|
||||
@@ -767,7 +767,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -778,7 +778,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register pylint problem matcher
|
||||
run: |
|
||||
@@ -812,7 +812,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -830,17 +830,17 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-mypy-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{
|
||||
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
|
||||
env.HA_SHORT_VERSION }}-
|
||||
- name: Register mypy problem matcher
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -900,7 +900,7 @@ jobs:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run split_tests.py
|
||||
run: |
|
||||
@@ -944,12 +944,13 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -959,7 +960,8 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register Python problem matcher
|
||||
run: |
|
||||
@@ -968,7 +970,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1019,6 +1021,12 @@ jobs:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1069,12 +1077,13 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libmariadb-dev-compat
|
||||
libmariadb-dev-compat \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1084,7 +1093,8 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register Python problem matcher
|
||||
run: |
|
||||
@@ -1152,6 +1162,12 @@ jobs:
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1200,7 +1216,8 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg
|
||||
libturbojpeg \
|
||||
libxml2-utils
|
||||
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||
sudo apt-get -y install \
|
||||
postgresql-server-dev-14
|
||||
@@ -1208,7 +1225,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1218,7 +1235,8 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register Python problem matcher
|
||||
run: |
|
||||
@@ -1287,6 +1305,12 @@ jobs:
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1312,12 +1336,12 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1354,12 +1378,13 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1369,7 +1394,8 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Register Python problem matcher
|
||||
run: |
|
||||
@@ -1432,6 +1458,12 @@ jobs:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1454,12 +1486,12 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -1479,7 +1511,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.13
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.13
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
16
.github/workflows/wheels.yml
vendored
16
.github/workflows/wheels.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -138,17 +138,17 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -187,22 +187,22 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
@@ -66,6 +66,7 @@ homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_devices.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
@@ -270,6 +271,7 @@ homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.immich.*
|
||||
homeassistant.components.incomfort.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
@@ -291,6 +293,7 @@ homeassistant.components.kaleidescape.*
|
||||
homeassistant.components.knocki.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.kulersky.*
|
||||
homeassistant.components.lacrosse.*
|
||||
homeassistant.components.lacrosse_view.*
|
||||
homeassistant.components.lamarzocco.*
|
||||
@@ -331,6 +334,7 @@ homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
homeassistant.components.met_eireann.*
|
||||
homeassistant.components.metoffice.*
|
||||
homeassistant.components.miele.*
|
||||
homeassistant.components.mikrotik.*
|
||||
homeassistant.components.min_max.*
|
||||
homeassistant.components.minecraft_server.*
|
||||
@@ -362,8 +366,10 @@ homeassistant.components.no_ip.*
|
||||
homeassistant.components.nordpool.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.ntfy.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
homeassistant.components.ohme.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
@@ -381,8 +387,10 @@ homeassistant.components.overseerr.*
|
||||
homeassistant.components.p1_monitor.*
|
||||
homeassistant.components.pandora.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.paperless_ngx.*
|
||||
homeassistant.components.peblar.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.pegel_online.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
@@ -429,7 +437,6 @@ homeassistant.components.roku.*
|
||||
homeassistant.components.romy.*
|
||||
homeassistant.components.rpi_power.*
|
||||
homeassistant.components.rss_feed_template.*
|
||||
homeassistant.components.rtsp_to_webrtc.*
|
||||
homeassistant.components.russound_rio.*
|
||||
homeassistant.components.ruuvi_gateway.*
|
||||
homeassistant.components.ruuvitag_ble.*
|
||||
@@ -459,6 +466,7 @@ homeassistant.components.slack.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.smlight.*
|
||||
homeassistant.components.smtp.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
|
70
CODEOWNERS
generated
70
CODEOWNERS
generated
@@ -46,8 +46,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/accuweather/ @bieniu
|
||||
/homeassistant/components/acmeda/ @atmurray
|
||||
/tests/components/acmeda/ @atmurray
|
||||
/homeassistant/components/adax/ @danielhiversen
|
||||
/tests/components/adax/ @danielhiversen
|
||||
/homeassistant/components/adax/ @danielhiversen @lazytarget
|
||||
/tests/components/adax/ @danielhiversen @lazytarget
|
||||
/homeassistant/components/adguard/ @frenck
|
||||
/tests/components/adguard/ @frenck
|
||||
/homeassistant/components/ads/ @mrpasztoradam
|
||||
@@ -89,6 +89,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/amazon_devices/ @chemelli74
|
||||
/tests/components/amazon_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
@@ -171,6 +173,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/avea/ @pattyland
|
||||
/homeassistant/components/awair/ @ahayworth @danielsjf
|
||||
/tests/components/awair/ @ahayworth @danielsjf
|
||||
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||
/tests/components/aws_s3/ @tomasbedrich
|
||||
/homeassistant/components/axis/ @Kane610
|
||||
/tests/components/axis/ @Kane610
|
||||
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||
@@ -200,8 +204,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/blebox/ @bbx-a @swistakm
|
||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||
/tests/components/blink/ @fronzbot @mkmer
|
||||
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
|
||||
/tests/components/blue_current/ @Floris272 @gleeuwen
|
||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
@@ -301,6 +305,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/crownstone/ @Crownstone @RicArch97
|
||||
/tests/components/crownstone/ @Crownstone @RicArch97
|
||||
/homeassistant/components/cups/ @fabaff
|
||||
/tests/components/cups/ @fabaff
|
||||
/homeassistant/components/daikin/ @fredrike
|
||||
/tests/components/daikin/ @fredrike
|
||||
/homeassistant/components/date/ @home-assistant/core
|
||||
@@ -432,7 +437,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50
|
||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
@@ -453,8 +458,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/evil_genius_labs/ @balloob
|
||||
/homeassistant/components/evohome/ @zxdavb
|
||||
/tests/components/evohome/ @zxdavb
|
||||
/homeassistant/components/ezviz/ @RenierM26 @baqs
|
||||
/tests/components/ezviz/ @RenierM26 @baqs
|
||||
/homeassistant/components/ezviz/ @RenierM26
|
||||
/tests/components/ezviz/ @RenierM26
|
||||
/homeassistant/components/faa_delays/ @ntilley905
|
||||
/tests/components/faa_delays/ @ntilley905
|
||||
/homeassistant/components/fan/ @home-assistant/core
|
||||
@@ -704,8 +709,12 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/image_upload/ @home-assistant/core
|
||||
/homeassistant/components/imap/ @jbouwh
|
||||
/tests/components/imap/ @jbouwh
|
||||
/homeassistant/components/imeon_inverter/ @Imeon-Energy
|
||||
/tests/components/imeon_inverter/ @Imeon-Energy
|
||||
/homeassistant/components/imgw_pib/ @bieniu
|
||||
/tests/components/imgw_pib/ @bieniu
|
||||
/homeassistant/components/immich/ @mib1185
|
||||
/tests/components/immich/ @mib1185
|
||||
/homeassistant/components/improv_ble/ @emontnemery
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
@@ -935,6 +944,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/metoffice/ @MrHarcombe @avee87
|
||||
/homeassistant/components/microbees/ @microBeesTech
|
||||
/tests/components/microbees/ @microBeesTech
|
||||
/homeassistant/components/miele/ @astrandb
|
||||
/tests/components/miele/ @astrandb
|
||||
/homeassistant/components/mikrotik/ @engrbm87
|
||||
/tests/components/mikrotik/ @engrbm87
|
||||
/homeassistant/components/mill/ @danielhiversen
|
||||
@@ -1047,6 +1058,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/nsw_fuel_station/ @nickw444
|
||||
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
/homeassistant/components/ntfy/ @tr4nt0r
|
||||
/tests/components/ntfy/ @tr4nt0r
|
||||
/homeassistant/components/nuheat/ @tstabrawa
|
||||
/tests/components/nuheat/ @tstabrawa
|
||||
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
|
||||
@@ -1075,8 +1088,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/oncue/ @bdraco @peterager
|
||||
/tests/components/oncue/ @bdraco @peterager
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onedrive/ @zweckj
|
||||
@@ -1105,8 +1116,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/opentherm_gw/ @mvn23
|
||||
/homeassistant/components/openuv/ @bachya
|
||||
/tests/components/openuv/ @bachya
|
||||
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi
|
||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
|
||||
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
|
||||
/homeassistant/components/opnsense/ @mtreinish
|
||||
/tests/components/opnsense/ @mtreinish
|
||||
/homeassistant/components/opower/ @tronikos
|
||||
@@ -1132,6 +1143,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/palazzetti/ @dotvav
|
||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||
/tests/components/panel_custom/ @home-assistant/frontend
|
||||
/homeassistant/components/paperless_ngx/ @fvgarrel
|
||||
/tests/components/paperless_ngx/ @fvgarrel
|
||||
/homeassistant/components/peblar/ @frenck
|
||||
/tests/components/peblar/ @frenck
|
||||
/homeassistant/components/peco/ @IceBotYT
|
||||
@@ -1170,6 +1183,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/homeassistant/components/private_ble_device/ @Jc2k
|
||||
/tests/components/private_ble_device/ @Jc2k
|
||||
/homeassistant/components/probe_plus/ @pantherale0
|
||||
/tests/components/probe_plus/ @pantherale0
|
||||
/homeassistant/components/profiler/ @bdraco
|
||||
/tests/components/profiler/ @bdraco
|
||||
/homeassistant/components/progettihwsw/ @ardaseremet
|
||||
@@ -1216,6 +1231,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/qnap_qsw/ @Noltari
|
||||
/tests/components/qnap_qsw/ @Noltari
|
||||
/homeassistant/components/quantum_gateway/ @cisasteelersfan
|
||||
/tests/components/quantum_gateway/ @cisasteelersfan
|
||||
/homeassistant/components/qvr_pro/ @oblogic7
|
||||
/homeassistant/components/qwikswitch/ @kellerza
|
||||
/tests/components/qwikswitch/ @kellerza
|
||||
@@ -1254,6 +1270,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
/tests/components/rehlko/ @bdraco @peterager
|
||||
/homeassistant/components/remote/ @home-assistant/core
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
/homeassistant/components/remote_calendar/ @Thomas55555
|
||||
@@ -1299,8 +1317,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rpi_power/ @shenxn @swetoast
|
||||
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
||||
/tests/components/rss_feed_template/ @home-assistant/core
|
||||
/homeassistant/components/rtsp_to_webrtc/ @allenporter
|
||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/homeassistant/components/russound_rio/ @noahhusby
|
||||
@@ -1387,7 +1403,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/siren/ @home-assistant/core @raman325
|
||||
/tests/components/siren/ @home-assistant/core @raman325
|
||||
/homeassistant/components/sisyphus/ @jkeljo
|
||||
/homeassistant/components/sky_hub/ @rogerselwyn
|
||||
/homeassistant/components/sky_remote/ @dunnmj @saty9
|
||||
/tests/components/sky_remote/ @dunnmj @saty9
|
||||
/homeassistant/components/skybell/ @tkdrob
|
||||
@@ -1405,6 +1420,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||
/homeassistant/components/smappee/ @bsmappee
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
|
||||
/tests/components/smarla/ @explicatis @rlint-explicatis
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @joostlek
|
||||
@@ -1434,8 +1451,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
||||
/tests/components/soma/ @ratsept @sebfortier2288
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
/tests/components/soma/ @ratsept
|
||||
/homeassistant/components/sonarr/ @ctalkington
|
||||
/tests/components/sonarr/ @ctalkington
|
||||
/homeassistant/components/songpal/ @rytilahti @shenxn
|
||||
@@ -1467,7 +1484,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/steam_online/ @tkdrob
|
||||
/homeassistant/components/steamist/ @bdraco
|
||||
/tests/components/steamist/ @bdraco
|
||||
/homeassistant/components/stiebel_eltron/ @fucm
|
||||
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/homeassistant/components/stookwijzer/ @fwestenberg
|
||||
/tests/components/stookwijzer/ @fwestenberg
|
||||
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
||||
@@ -1478,8 +1496,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/subaru/ @G-Two
|
||||
/homeassistant/components/suez_water/ @ooii @jb101010-2
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sun/ @home-assistant/core
|
||||
/tests/components/sun/ @home-assistant/core
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
@@ -1492,8 +1510,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/switch_as_x/ @home-assistant/core
|
||||
/homeassistant/components/switchbee/ @jafar-atili
|
||||
/tests/components/switchbee/ @jafar-atili
|
||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/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
|
||||
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
|
||||
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
|
||||
@@ -1533,8 +1551,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tedee/ @patrickhilker @zweckj
|
||||
/homeassistant/components/tellduslive/ @fredrike
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
/tests/components/tesla_fleet/ @Bre77
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
@@ -1670,8 +1688,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam
|
||||
/tests/components/voip/ @balloob @synesthesiam
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
/tests/components/volumio/ @OnFreund
|
||||
/homeassistant/components/volvooncall/ @molobrakos
|
||||
@@ -1788,6 +1806,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/zeversolar/ @kvanzuijlen
|
||||
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/homeassistant/components/zimi/ @markhannon
|
||||
/tests/components/zimi/ @markhannon
|
||||
/homeassistant/components/zodiac/ @JulienTant
|
||||
/tests/components/zodiac/ @JulienTant
|
||||
/homeassistant/components/zone/ @home-assistant/core
|
||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.10
|
||||
RUN pip3 install uv==0.7.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@@ -53,6 +53,7 @@ from .components import (
|
||||
logbook as logbook_pre_import, # noqa: F401
|
||||
lovelace as lovelace_pre_import, # noqa: F401
|
||||
onboarding as onboarding_pre_import, # noqa: F401
|
||||
person as person_pre_import, # noqa: F401
|
||||
recorder as recorder_import, # noqa: F401 - not named pre_import since it has requirements
|
||||
repairs as repairs_pre_import, # noqa: F401
|
||||
search as search_pre_import, # noqa: F401
|
||||
@@ -859,8 +860,14 @@ async def _async_set_up_integrations(
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
# Detect all cycles
|
||||
integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, all_integrations.values(), set(all_integrations)
|
||||
)
|
||||
)
|
||||
all_domains = set(integrations_after_dependencies)
|
||||
domains = set(integrations) & all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
@@ -868,6 +875,8 @@ async def _async_set_up_integrations(
|
||||
all_domains - domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, all_domains)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
@@ -900,24 +909,12 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
for dep in integrations_after_dependencies[domain]
|
||||
if dep not in stage_domains
|
||||
}
|
||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||
|
||||
stage_all_domains = stage_domains | stage_dep_domains
|
||||
stage_all_integrations = {
|
||||
domain: all_integrations[domain] for domain in stage_all_domains
|
||||
}
|
||||
# Detect all cycles
|
||||
stage_integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, stage_all_integrations.values(), stage_all_domains
|
||||
)
|
||||
)
|
||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
||||
stage_domains &= stage_all_domains
|
||||
stage_dep_domains &= stage_all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||
@@ -928,8 +925,6 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
|
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"domain": "amazon",
|
||||
"name": "Amazon",
|
||||
"integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"]
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"amazon_devices",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
"fire_tv",
|
||||
"route53"
|
||||
]
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@
|
||||
"google_assistant_sdk",
|
||||
"google_cloud",
|
||||
"google_drive",
|
||||
"google_gemini",
|
||||
"google_generative_ai_conversation",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
|
6
homeassistant/brands/nuki.json
Normal file
6
homeassistant/brands/nuki.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "nuki",
|
||||
"name": "Nuki",
|
||||
"integrations": ["nuki"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
||||
2: "moderate",
|
||||
3: "high",
|
||||
4: "very_high",
|
||||
5: "extreme",
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
@@ -72,10 +72,11 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"extreme": "Extreme",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "Moderate",
|
||||
"very_high": "Very high"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +90,11 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,10 +125,11 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,10 +170,11 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,10 +185,11 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,10 +200,11 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,25 +2,38 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONNECTION_TYPE, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
"""Set up Adax from a config entry."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
local_coordinator = AdaxLocalCoordinator(hass, entry)
|
||||
entry.runtime_data = local_coordinator
|
||||
else:
|
||||
cloud_coordinator = AdaxCloudCoordinator(hass, entry)
|
||||
entry.runtime_data = cloud_coordinator
|
||||
|
||||
await entry.runtime_data.async_config_entry_first_refresh()
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AdaxConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
# convert title and unique_id to string
|
||||
if config_entry.version == 1:
|
||||
|
@@ -12,57 +12,42 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_UNIQUE_ID,
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from . import AdaxConfigEntry
|
||||
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxLocalCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax thermostat with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
adax_data_handler = AdaxLocal(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
entry.data[CONF_TOKEN],
|
||||
websession=async_get_clientsession(hass, verify_ssl=False),
|
||||
)
|
||||
local_coordinator = cast(AdaxLocalCoordinator, entry.runtime_data)
|
||||
async_add_entities(
|
||||
[LocalAdaxDevice(adax_data_handler, entry.data[CONF_UNIQUE_ID])], True
|
||||
[LocalAdaxDevice(local_coordinator, entry.data[CONF_UNIQUE_ID])],
|
||||
)
|
||||
else:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
async_add_entities(
|
||||
AdaxDevice(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
return
|
||||
|
||||
adax_data_handler = Adax(
|
||||
entry.data[ACCOUNT_ID],
|
||||
entry.data[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
AdaxDevice(room, adax_data_handler)
|
||||
for room in await adax_data_handler.get_rooms()
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class AdaxDevice(ClimateEntity):
|
||||
class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
@@ -76,20 +61,37 @@ class AdaxDevice(ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the heater."""
|
||||
self._device_id = heater_data["id"]
|
||||
self._adax_data_handler = adax_data_handler
|
||||
super().__init__(coordinator)
|
||||
self._adax_data_handler: Adax = coordinator.adax_data_handler
|
||||
self._device_id = device_id
|
||||
|
||||
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
|
||||
self._attr_name = self.room["name"]
|
||||
self._attr_unique_id = f"{self.room['homeId']}_{self._device_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, heater_data["id"])},
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
# Instead of setting the device name to the entity name, adax
|
||||
# should be updated to set has_entity_name = True, and set the entity
|
||||
# name to None
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="Adax",
|
||||
)
|
||||
self._apply_data(self.room)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Whether the entity is available or not."""
|
||||
return super().available and self._device_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def room(self) -> dict[str, Any]:
|
||||
"""Gets the data for this particular device."""
|
||||
return self.coordinator.data[self._device_id]
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
@@ -104,7 +106,9 @@ class AdaxDevice(ClimateEntity):
|
||||
)
|
||||
else:
|
||||
return
|
||||
await self._adax_data_handler.update()
|
||||
|
||||
# Request data refresh from source to verify that update was successful
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
@@ -114,28 +118,31 @@ class AdaxDevice(ClimateEntity):
|
||||
self._device_id, temperature, True
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
for room in await self._adax_data_handler.get_rooms():
|
||||
if room["id"] != self._device_id:
|
||||
continue
|
||||
self._attr_name = room["name"]
|
||||
self._attr_current_temperature = room.get("temperature")
|
||||
self._attr_target_temperature = room.get("targetTemperature")
|
||||
if room["heatingEnabled"]:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
return
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if room := self.room:
|
||||
self._apply_data(room)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _apply_data(self, room: dict[str, Any]) -> None:
|
||||
"""Update the appropriate attributues based on received data."""
|
||||
self._attr_current_temperature = room.get("temperature")
|
||||
self._attr_target_temperature = room.get("targetTemperature")
|
||||
if room["heatingEnabled"]:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
|
||||
|
||||
class LocalAdaxDevice(ClimateEntity):
|
||||
class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_hvac_mode = HVACMode.HEAT
|
||||
_attr_hvac_mode = HVACMode.OFF
|
||||
_attr_icon = "mdi:radiator-off"
|
||||
_attr_max_temp = 35
|
||||
_attr_min_temp = 5
|
||||
_attr_supported_features = (
|
||||
@@ -146,9 +153,10 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
|
||||
def __init__(self, coordinator: AdaxLocalCoordinator, unique_id: str) -> None:
|
||||
"""Initialize the heater."""
|
||||
self._adax_data_handler = adax_data_handler
|
||||
super().__init__(coordinator)
|
||||
self._adax_data_handler: AdaxLocal = coordinator.adax_data_handler
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
@@ -169,17 +177,20 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
return
|
||||
await self._adax_data_handler.set_target_temperature(temperature)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
data = await self._adax_data_handler.get_status()
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
self._attr_available = self._attr_current_temperature is not None
|
||||
if (target_temp := data["target_temperature"]) == 0:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
if target_temp == 0:
|
||||
self._attr_target_temperature = self._attr_min_temp
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
self._attr_target_temperature = target_temp
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if data := self.coordinator.data:
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
self._attr_available = self._attr_current_temperature is not None
|
||||
if (target_temp := data["target_temperature"]) == 0:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
if target_temp == 0:
|
||||
self._attr_target_temperature = self._attr_min_temp
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
self._attr_target_temperature = target_temp
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Constants for the Adax integration."""
|
||||
|
||||
import datetime
|
||||
from typing import Final
|
||||
|
||||
ACCOUNT_ID: Final = "account_id"
|
||||
@@ -9,3 +10,5 @@ DOMAIN: Final = "adax"
|
||||
LOCAL = "Local"
|
||||
WIFI_SSID = "wifi_ssid"
|
||||
WIFI_PSWD = "wifi_pswd"
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=60)
|
||||
|
71
homeassistant/components/adax/coordinator.py
Normal file
71
homeassistant/components/adax/coordinator.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""DataUpdateCoordinator for the Adax component."""
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from adax import Adax
|
||||
from adax_local import Adax as AdaxLocal
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ACCOUNT_ID, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type AdaxConfigEntry = ConfigEntry[AdaxCloudCoordinator | AdaxLocalCoordinator]
|
||||
|
||||
|
||||
class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
"""Coordinator for updating data to and from Adax (cloud)."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
|
||||
"""Initialize the Adax coordinator used for Cloud mode."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry=entry,
|
||||
logger=_LOGGER,
|
||||
name="AdaxCloud",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.adax_data_handler = Adax(
|
||||
entry.data[ACCOUNT_ID],
|
||||
entry.data[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
class AdaxLocalCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
|
||||
"""Coordinator for updating data to and from Adax (local)."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
|
||||
"""Initialize the Adax coordinator used for Local mode."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry=entry,
|
||||
logger=_LOGGER,
|
||||
name="AdaxLocal",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.adax_data_handler = AdaxLocal(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
entry.data[CONF_TOKEN],
|
||||
websession=async_get_clientsession(hass, verify_ssl=False),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the Adax."""
|
||||
if result := await self.adax_data_handler.get_status():
|
||||
return cast(dict[str, Any], result)
|
||||
raise UpdateFailed("Got invalid status from device")
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "adax",
|
||||
"name": "Adax",
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"codeowners": ["@danielhiversen", "@lazytarget"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"iot_class": "local_polling",
|
||||
|
@@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
|
||||
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
|
||||
self._id: str = light["id"]
|
||||
self._attr_unique_id += f"-{self._id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)},
|
||||
via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]),
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
via_device=(DOMAIN, self.coordinator.data["system"]["rid"]),
|
||||
manufacturer="Advantage Air",
|
||||
model=light.get("moduleType"),
|
||||
name=light["name"],
|
||||
|
@@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .entity import AdvantageAirEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -32,9 +32,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
|
||||
"""Initialize the Advantage Air App."""
|
||||
super().__init__(instance)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"])
|
||||
},
|
||||
identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
|
||||
manufacturer="Advantage Air",
|
||||
model=self.coordinator.data["system"]["sysType"],
|
||||
name=self.coordinator.data["system"]["name"],
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
|
||||
from .const import DOMAIN, SERVER_URL
|
||||
|
||||
ATTRIBUTION = "ispyconnect.com"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
@@ -46,7 +46,7 @@ async def async_setup_entry(
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(AGENT_DOMAIN, agent_client.unique)},
|
||||
identifiers={(DOMAIN, agent_client.unique)},
|
||||
manufacturer="iSpyConnect",
|
||||
name=f"Agent {agent_client.name}",
|
||||
model="Agent DVR",
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import DOMAIN as AGENT_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
CONF_HOME_MODE_NAME = "home"
|
||||
CONF_AWAY_MODE_NAME = "away"
|
||||
@@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
||||
self._client = client
|
||||
self._attr_unique_id = f"{client.unique}_CP"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AGENT_DOMAIN, client.unique)},
|
||||
identifiers={(DOMAIN, client.unique)},
|
||||
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
|
||||
manufacturer="Agent",
|
||||
model=CONST_ALARM_CONTROL_PANEL_NAME,
|
||||
|
@@ -6,6 +6,7 @@ from typing import Any, Concatenate
|
||||
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -29,6 +30,7 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
||||
model_id=measures.model,
|
||||
serial_number=coordinator.serial_number,
|
||||
sw_version=measures.firmware_version,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
|
||||
)
|
||||
|
||||
|
||||
|
@@ -68,8 +68,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "LED bar mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"co2": "Carbon dioxide",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "Particulate matter"
|
||||
}
|
||||
},
|
||||
@@ -143,8 +143,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||
"state": {
|
||||
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
||||
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||
}
|
||||
},
|
||||
|
@@ -3,6 +3,19 @@
|
||||
"name": "Airthings",
|
||||
"codeowners": ["@danielhiversen", "@LaStrada"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "airthings-view"
|
||||
},
|
||||
{
|
||||
"hostname": "airthings-hub",
|
||||
"macaddress": "D0141190*"
|
||||
},
|
||||
{
|
||||
"hostname": "airthings-hub",
|
||||
"macaddress": "70B3D52A0*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["airthings"],
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
@@ -78,6 +79,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
translation_key="light",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"lux": SensorEntityDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"virusRisk": SensorEntityDescription(
|
||||
key="virusRisk",
|
||||
translation_key="virus_risk",
|
||||
|
@@ -142,7 +142,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
||||
return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode]
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return the list of available operation modes."""
|
||||
airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number)
|
||||
modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes]
|
||||
@@ -226,12 +226,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
return super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
def min_temp(self) -> float:
|
||||
"""Return Minimum Temperature for AC of this group."""
|
||||
return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
def max_temp(self) -> float:
|
||||
"""Return Max Temperature for AC of this group."""
|
||||
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
|
||||
|
||||
|
@@ -16,8 +16,8 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "State"
|
||||
"state": "State",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur dioxide"
|
||||
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"p1": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"p2": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
@@ -9,6 +9,8 @@ from aioairzone.const import (
|
||||
AZD_HUMIDITY,
|
||||
AZD_TEMP,
|
||||
AZD_TEMP_UNIT,
|
||||
AZD_THERMOSTAT_BATTERY,
|
||||
AZD_THERMOSTAT_SIGNAL,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WIFI_RSSI,
|
||||
AZD_ZONES,
|
||||
@@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
key=AZD_THERMOSTAT_BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_THERMOSTAT_SIGNAL,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="thermostat_signal",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -76,6 +76,9 @@
|
||||
"sensor": {
|
||||
"rssi": {
|
||||
"name": "RSSI"
|
||||
},
|
||||
"thermostat_signal": {
|
||||
"name": "Signal strength"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.11"]
|
||||
"requirements": ["aioairzone-cloud==0.6.12"]
|
||||
}
|
||||
|
@@ -32,9 +32,9 @@
|
||||
"air_quality": {
|
||||
"name": "Air Quality mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"auto": "Auto"
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"auto": "[%key:common::state::auto%]"
|
||||
}
|
||||
},
|
||||
"modes": {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Choose AlarmDecoder Protocol",
|
||||
"title": "Choose AlarmDecoder protocol",
|
||||
"data": {
|
||||
"protocol": "Protocol"
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"device_baudrate": "Device Baud Rate",
|
||||
"device_path": "Device Path"
|
||||
"device_baudrate": "Device baud rate",
|
||||
"device_path": "Device path"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
|
||||
@@ -44,36 +44,36 @@
|
||||
"arm_settings": {
|
||||
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
|
||||
"data": {
|
||||
"auto_bypass": "Auto Bypass on Arm",
|
||||
"code_arm_required": "Code Required for Arming",
|
||||
"alt_night_mode": "Alternative Night Mode"
|
||||
"auto_bypass": "Auto-bypass on arm",
|
||||
"code_arm_required": "Code required for arming",
|
||||
"alt_night_mode": "Alternative night mode"
|
||||
}
|
||||
},
|
||||
"zone_select": {
|
||||
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
|
||||
"description": "Enter the zone number you'd like to to add, edit, or remove.",
|
||||
"data": {
|
||||
"zone_number": "Zone Number"
|
||||
"zone_number": "Zone number"
|
||||
}
|
||||
},
|
||||
"zone_details": {
|
||||
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
|
||||
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
|
||||
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.",
|
||||
"data": {
|
||||
"zone_name": "Zone Name",
|
||||
"zone_type": "Zone Type",
|
||||
"zone_rfid": "RF Serial",
|
||||
"zone_loop": "RF Loop",
|
||||
"zone_relayaddr": "Relay Address",
|
||||
"zone_relaychan": "Relay Channel"
|
||||
"zone_name": "Zone name",
|
||||
"zone_type": "Zone type",
|
||||
"zone_rfid": "RF serial",
|
||||
"zone_loop": "RF loop",
|
||||
"zone_relayaddr": "Relay address",
|
||||
"zone_relaychan": "Relay channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.",
|
||||
"relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.",
|
||||
"int": "The field below must be an integer.",
|
||||
"loop_rfid": "RF Loop cannot be used without RF Serial.",
|
||||
"loop_range": "RF Loop must be an integer between 1 and 4."
|
||||
"loop_rfid": "'RF loop' cannot be used without 'RF serial'.",
|
||||
"loop_range": "'RF loop' must be an integer between 1 and 4."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||
class MediaPlayerCapabilities(AlexaEntity):
|
||||
"""Class to represent MediaPlayer capabilities."""
|
||||
|
||||
@@ -757,9 +757,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
|
||||
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
|
||||
inputs = AlexaInputController.get_valid_inputs(
|
||||
self.entity.attributes.get(
|
||||
media_player.const.ATTR_INPUT_SOURCE_LIST, []
|
||||
)
|
||||
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaInputController(self.entity)
|
||||
@@ -776,8 +774,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
and domain != "denonavr"
|
||||
):
|
||||
inputs = AlexaEqualizerController.get_valid_inputs(
|
||||
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
or []
|
||||
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaEqualizerController(self.entity)
|
||||
|
@@ -566,7 +566,7 @@ async def async_api_set_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -589,7 +589,7 @@ async def async_api_select_input(
|
||||
|
||||
# Attempt to map the ALL UPPERCASE payload name to a source.
|
||||
# Strips trailing 1 to match single input devices.
|
||||
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
|
||||
source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
|
||||
for source in source_list:
|
||||
formatted_source = (
|
||||
source.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||||
@@ -611,7 +611,7 @@ async def async_api_select_input(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
media_player.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -636,7 +636,7 @@ async def async_api_adjust_volume(
|
||||
volume_delta = int(directive.payload["volume"])
|
||||
|
||||
entity = directive.entity
|
||||
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
|
||||
# read current state
|
||||
try:
|
||||
@@ -648,7 +648,7 @@ async def async_api_adjust_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -709,7 +709,7 @@ async def async_api_set_mute(
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1708,15 +1708,13 @@ async def async_api_changechannel(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
media_player.const.MEDIA_TYPE_CHANNEL
|
||||
),
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
media_player.const.SERVICE_PLAY_MEDIA,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
@@ -1825,13 +1823,13 @@ async def async_api_set_eq_mode(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
mode: str = directive.payload["mode"]
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
||||
data[media_player.ATTR_SOUND_MODE] = mode.lower()
|
||||
else:
|
||||
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
@@ -3,10 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -260,10 +260,10 @@ async def async_enable_proactive_mode(
|
||||
def extra_significant_check(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
|
||||
old_attrs: Mapping[Any, Any],
|
||||
old_extra_arg: Any,
|
||||
new_state: str,
|
||||
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
|
||||
new_attrs: Mapping[Any, Any],
|
||||
new_extra_arg: Any,
|
||||
) -> bool:
|
||||
"""Check if the serialized data has changed."""
|
||||
|
32
homeassistant/components/amazon_devices/__init__.py
Normal file
32
homeassistant/components/amazon_devices/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Amazon Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NOTIFY,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Amazon Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.api.close()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
71
homeassistant/components/amazon_devices/binary_sensor.py
Normal file
71
homeassistant/components/amazon_devices/binary_sensor.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Support for binary sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Amazon Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices binary sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
"""Binary sensor device."""
|
||||
|
||||
entity_description: AmazonBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
63
homeassistant/components/amazon_devices/config_flow.py
Normal file
63
homeassistant/components/amazon_devices/config_flow.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Config flow for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Amazon Devices."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
client = AmazonEchoApi(
|
||||
user_input[CONF_COUNTRY],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input.pop(CONF_CODE)
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input | {CONF_LOGIN_DATA: data},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY, default=self.hass.config.country
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.positive_int,
|
||||
}
|
||||
),
|
||||
)
|
8
homeassistant/components/amazon_devices/const.py
Normal file
8
homeassistant/components/amazon_devices/const.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Amazon Devices constants."""
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "amazon_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
58
homeassistant/components/amazon_devices/coordinator.py
Normal file
58
homeassistant/components/amazon_devices/coordinator.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Support for Amazon Devices."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||
from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Base coordinator for Amazon Devices."""
|
||||
|
||||
config_entry: AmazonConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=entry.title,
|
||||
config_entry=entry,
|
||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
entry.data[CONF_COUNTRY],
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
try:
|
||||
await self.api.login_mode_stored_data()
|
||||
return await self.api.get_devices_data()
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
57
homeassistant/components/amazon_devices/entity.py
Normal file
57
homeassistant/components/amazon_devices/entity.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL, SPEAKER_GROUP_MODEL
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
|
||||
|
||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
serial_num: str,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details: dict[str, str] = cast(
|
||||
"dict", DEVICE_TYPE_TO_MODEL.get(self.device.device_type)
|
||||
)
|
||||
model = model_details["model"] if model_details else None
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer="Amazon",
|
||||
hw_version=model_details["hw_version"] if model_details else None,
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@property
|
||||
def device(self) -> AmazonDevice:
|
||||
"""Return the device."""
|
||||
return self.coordinator.data[self._serial_num]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._serial_num in self.coordinator.data
|
12
homeassistant/components/amazon_devices/icons.json
Normal file
12
homeassistant/components/amazon_devices/icons.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth",
|
||||
"state": {
|
||||
"off": "mdi:bluetooth-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
homeassistant/components/amazon_devices/manifest.json
Normal file
33
homeassistant/components/amazon_devices/manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"domain": "amazon_devices",
|
||||
"name": "Amazon Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "EC2BEB*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==2.0.1"]
|
||||
}
|
74
homeassistant/components/amazon_devices/notify.py
Normal file
74
homeassistant/components/amazon_devices/notify.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Support for notification entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||
|
||||
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Amazon Devices notify entity description."""
|
||||
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
|
||||
|
||||
NOTIFY: Final = (
|
||||
AmazonNotifyEntityDescription(
|
||||
key="speak",
|
||||
translation_key="speak",
|
||||
subkey="AUDIO_PLAYER",
|
||||
method=lambda api, device, message: api.call_alexa_speak(device, message),
|
||||
),
|
||||
AmazonNotifyEntityDescription(
|
||||
key="announce",
|
||||
translation_key="announce",
|
||||
subkey="AUDIO_PLAYER",
|
||||
method=lambda api, device, message: api.call_alexa_announcement(
|
||||
device, message
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices notification entity based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in NOTIFY
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
)
|
||||
|
||||
|
||||
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
"""Binary sensor notify platform."""
|
||||
|
||||
entity_description: AmazonNotifyEntityDescription
|
||||
|
||||
async def async_send_message(
|
||||
self, message: str, title: str | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
"""Send a message."""
|
||||
|
||||
await self.entity_description.method(self.coordinator.api, self.device, message)
|
74
homeassistant/components/amazon_devices/quality_scale.yaml
Normal file
74
homeassistant/components/amazon_devices/quality_scale.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: entities do not explicitly subscribe to events
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Network information not relevant
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: no known use cases for repair issues or flows, yet
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: automate the cleanup process
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: done
|
60
homeassistant/components/amazon_devices/strings.json
Normal file
60
homeassistant/components/amazon_devices/strings.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"common": {
|
||||
"data_country": "Country code",
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password sent to your email address."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::amazon_devices::common::data_country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::amazon_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::amazon_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::amazon_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"speak": {
|
||||
"name": "Speak"
|
||||
},
|
||||
"announce": {
|
||||
"name": "Announce"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
84
homeassistant/components/amazon_devices/switch.py
Normal file
84
homeassistant/components/amazon_devices/switch.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Support for switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Amazon Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
method: str
|
||||
|
||||
|
||||
SWITCHES: Final = (
|
||||
AmazonSwitchEntityDescription(
|
||||
key="do_not_disturb",
|
||||
subkey="AUDIO_PLAYER",
|
||||
translation_key="do_not_disturb",
|
||||
is_on_fn=lambda _device: _device.do_not_disturb,
|
||||
method="set_do_not_disturb",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices switches based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||
for switch_desc in SWITCHES
|
||||
for serial_num in coordinator.data
|
||||
if switch_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
)
|
||||
|
||||
|
||||
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
"""Switch device."""
|
||||
|
||||
entity_description: AmazonSwitchEntityDescription
|
||||
|
||||
async def _switch_set_state(self, state: bool) -> None:
|
||||
"""Set desired switch state."""
|
||||
method = getattr(self.coordinator.api, self.entity_description.method)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._switch_set_state(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._switch_set_state(False)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["boto3==1.34.131"]
|
||||
"requirements": ["boto3==1.37.1"]
|
||||
}
|
||||
|
@@ -3,12 +3,12 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_addons": "Addons",
|
||||
"tracked_addons": "Add-ons",
|
||||
"tracked_integrations": "Integrations",
|
||||
"tracked_custom_integrations": "Custom integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "Select the addons you want to track",
|
||||
"tracked_addons": "Select the add-ons you want to track",
|
||||
"tracked_integrations": "Select the integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pydroid-ipcam==2.0.0"]
|
||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||
}
|
||||
|
@@ -73,7 +73,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
||||
self._api.send_key_command(key_code, direction)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||
) from exc
|
||||
|
||||
def _send_launch_app_command(self, app_link: str) -> None:
|
||||
@@ -85,5 +85,5 @@ class AndroidTVRemoteBaseEntity(Entity):
|
||||
self._api.send_launch_app_command(app_link)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||
) from exc
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"requirements": ["androidtvremote2==0.2.1"],
|
||||
"requirements": ["androidtvremote2==0.2.2"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -233,5 +233,5 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
await asyncio.sleep(delay_secs)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
translation_domain=DOMAIN, translation_key="connection_closed"
|
||||
) from exc
|
||||
|
@@ -51,8 +51,17 @@
|
||||
"app_id": "Application ID",
|
||||
"app_icon": "Application icon",
|
||||
"app_delete": "Check to delete this application"
|
||||
},
|
||||
"data_description": {
|
||||
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
|
||||
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"connection_closed": {
|
||||
"message": "Connection to the Android TV device is closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
@@ -52,7 +53,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
|
||||
RECOMMENDED_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
|
||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
||||
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
||||
}
|
||||
|
||||
@@ -134,9 +135,8 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if user_input.get(
|
||||
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
|
||||
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
|
||||
@@ -151,12 +151,16 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
|
||||
suggested_values = options.copy()
|
||||
if not suggested_values.get(CONF_PROMPT):
|
||||
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
|
||||
if (
|
||||
suggested_llm_apis := suggested_values.get(CONF_LLM_HASS_API)
|
||||
) and isinstance(suggested_llm_apis, str):
|
||||
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(anthropic_config_option_schema(self.hass, options)),
|
||||
@@ -172,28 +176,22 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
|
||||
def anthropic_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
||||
options: Mapping[str, Any],
|
||||
) -> dict:
|
||||
"""Return a schema for Anthropic completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label="No control",
|
||||
value="none",
|
||||
)
|
||||
]
|
||||
hass_apis.extend(
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
)
|
||||
]
|
||||
|
||||
schema = {
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector(
|
||||
SelectSelectorConfig(options=hass_apis)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
|
@@ -17,4 +17,11 @@ CONF_THINKING_BUDGET = "thinking_budget"
|
||||
RECOMMENDED_THINKING_BUDGET = 0
|
||||
MIN_THINKING_BUDGET = 1024
|
||||
|
||||
THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-3-7-sonnet-latest"]
|
||||
THINKING_MODELS = [
|
||||
"claude-3-7-sonnet-20250219",
|
||||
"claude-3-7-sonnet-latest",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-0",
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-sonnet-4-0",
|
||||
]
|
||||
|
@@ -9,11 +9,13 @@ from anthropic import AsyncStream
|
||||
from anthropic._types import NOT_GIVEN
|
||||
from anthropic.types import (
|
||||
InputJSONDelta,
|
||||
MessageDeltaUsage,
|
||||
MessageParam,
|
||||
MessageStreamEvent,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
RawMessageDeltaEvent,
|
||||
RawMessageStartEvent,
|
||||
RawMessageStopEvent,
|
||||
RedactedThinkingBlock,
|
||||
@@ -31,6 +33,7 @@ from anthropic.types import (
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlock,
|
||||
ToolUseBlockParam,
|
||||
Usage,
|
||||
)
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -162,7 +165,8 @@ def _convert_content(
|
||||
return messages
|
||||
|
||||
|
||||
async def _transform_stream(
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
chat_log: conversation.ChatLog,
|
||||
result: AsyncStream[MessageStreamEvent],
|
||||
messages: list[MessageParam],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
@@ -207,6 +211,7 @@ async def _transform_stream(
|
||||
| None
|
||||
) = None
|
||||
current_tool_args: str
|
||||
input_usage: Usage | None = None
|
||||
|
||||
async for response in result:
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
@@ -215,6 +220,7 @@ async def _transform_stream(
|
||||
if response.message.role != "assistant":
|
||||
raise ValueError("Unexpected message role")
|
||||
current_message = MessageParam(role=response.message.role, content=[])
|
||||
input_usage = response.message.usage
|
||||
elif isinstance(response, RawContentBlockStartEvent):
|
||||
if isinstance(response.content_block, ToolUseBlock):
|
||||
current_block = ToolUseBlockParam(
|
||||
@@ -265,32 +271,56 @@ async def _transform_stream(
|
||||
if current_block is None:
|
||||
raise ValueError("Unexpected stop event without a current block")
|
||||
if current_block["type"] == "tool_use":
|
||||
tool_block = cast(ToolUseBlockParam, current_block)
|
||||
# tool block
|
||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||
tool_block["input"] = tool_args
|
||||
current_block["input"] = tool_args
|
||||
yield {
|
||||
"tool_calls": [
|
||||
llm.ToolInput(
|
||||
id=tool_block["id"],
|
||||
tool_name=tool_block["name"],
|
||||
id=current_block["id"],
|
||||
tool_name=current_block["name"],
|
||||
tool_args=tool_args,
|
||||
)
|
||||
]
|
||||
}
|
||||
elif current_block["type"] == "thinking":
|
||||
thinking_block = cast(ThinkingBlockParam, current_block)
|
||||
LOGGER.debug("Thinking: %s", thinking_block["thinking"])
|
||||
# thinking block
|
||||
LOGGER.debug("Thinking: %s", current_block["thinking"])
|
||||
|
||||
if current_message is None:
|
||||
raise ValueError("Unexpected stop event without a current message")
|
||||
current_message["content"].append(current_block) # type: ignore[union-attr]
|
||||
current_block = None
|
||||
elif isinstance(response, RawMessageDeltaEvent):
|
||||
if (usage := response.usage) is not None:
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
if response.delta.stop_reason == "refusal":
|
||||
raise HomeAssistantError("Potential policy violation detected")
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if current_message is not None:
|
||||
messages.append(current_message)
|
||||
current_message = None
|
||||
|
||||
|
||||
def _create_token_stats(
|
||||
input_usage: Usage | None, response_usage: MessageDeltaUsage
|
||||
) -> dict[str, Any]:
|
||||
"""Create token stats for conversation agent tracing."""
|
||||
input_tokens = 0
|
||||
cached_input_tokens = 0
|
||||
if input_usage:
|
||||
input_tokens = input_usage.input_tokens
|
||||
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
|
||||
output_tokens = response_usage.output_tokens
|
||||
return {
|
||||
"stats": {
|
||||
"input_tokens": input_tokens,
|
||||
"cached_input_tokens": cached_input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnthropicConversationEntity(
|
||||
conversation.ConversationEntity, conversation.AbstractConversationAgent
|
||||
):
|
||||
@@ -298,6 +328,7 @@ class AnthropicConversationEntity(
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supports_streaming = True
|
||||
|
||||
def __init__(self, entry: AnthropicConfigEntry) -> None:
|
||||
"""Initialize the agent."""
|
||||
@@ -393,7 +424,8 @@ class AnthropicConversationEntity(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
user_input.agent_id, _transform_stream(stream, messages)
|
||||
user_input.agent_id,
|
||||
_transform_stream(chat_log, stream, messages),
|
||||
)
|
||||
if not isinstance(content, conversation.AssistantContent)
|
||||
]
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.47.2"]
|
||||
"requirements": ["anthropic==0.52.0"]
|
||||
}
|
||||
|
@@ -53,10 +53,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
|
||||
"""Initialize the APCUPSd binary device."""
|
||||
super().__init__(coordinator, context=description.key.upper())
|
||||
|
||||
# Set up unique id and device info if serial number is available.
|
||||
if (serial_no := coordinator.data.serial_no) is not None:
|
||||
self._attr_unique_id = f"{serial_no}_{description.key}"
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
@property
|
||||
|
@@ -46,11 +46,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(step_id="user", data_schema=_SCHEMA)
|
||||
|
||||
host, port = user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
|
||||
# Abort if an entry with same host and port is present.
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
|
||||
# Test the connection to the host and get the current status for serial number.
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
@@ -67,3 +63,30 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
title = data.name or data.model or data.serial_no or "APC UPS"
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry."""
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reconfigure", data_schema=_SCHEMA)
|
||||
|
||||
host, port = user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
except (OSError, asyncio.IncompleteReadError, TimeoutError):
|
||||
errors = {"base": "cannot_connect"}
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
await self.async_set_unique_id(data.serial_no)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_apcupsd_daemon")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
@@ -85,11 +85,16 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
@property
|
||||
def unique_device_id(self) -> str:
|
||||
"""Return a unique ID of the device, which is the serial number (if available) or the config entry ID."""
|
||||
return self.data.serial_no or self.config_entry.entry_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.data.serial_no or self.config_entry.entry_id)},
|
||||
identifiers={(DOMAIN, self.unique_device_id)},
|
||||
model=self.data.model,
|
||||
manufacturer="APC",
|
||||
name=self.data.name or "APC UPS",
|
||||
@@ -108,4 +113,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
||||
data = await aioapcaccess.request_status(self._host, self._port)
|
||||
return APCUPSdData(data)
|
||||
except (OSError, asyncio.IncompleteReadError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from error
|
||||
|
@@ -458,11 +458,8 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, context=description.key.upper())
|
||||
|
||||
# Set up unique id and device info if serial number is available.
|
||||
if (serial_no := coordinator.data.serial_no) is not None:
|
||||
self._attr_unique_id = f"{serial_no}_{description.key}"
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
# Initial update of attributes.
|
||||
|
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"wrong_apcupsd_daemon": "The reconfigured APC UPS Daemon is not the same as the one already configured.",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
@@ -93,7 +95,7 @@
|
||||
"name": "Internal temperature"
|
||||
},
|
||||
"last_self_test": {
|
||||
"name": "Last self test"
|
||||
"name": "Last self-test"
|
||||
},
|
||||
"last_transfer": {
|
||||
"name": "Last transfer"
|
||||
@@ -177,7 +179,7 @@
|
||||
"name": "Restore requirement"
|
||||
},
|
||||
"self_test_result": {
|
||||
"name": "Self test result"
|
||||
"name": "Self-test result"
|
||||
},
|
||||
"sensitivity": {
|
||||
"name": "Sensitivity"
|
||||
@@ -195,7 +197,7 @@
|
||||
"name": "Status"
|
||||
},
|
||||
"self_test_interval": {
|
||||
"name": "Self test interval"
|
||||
"name": "Self-test interval"
|
||||
},
|
||||
"time_left": {
|
||||
"name": "Time left"
|
||||
@@ -219,5 +221,10 @@
|
||||
"name": "Transfer to battery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to APC UPS Daemon."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -120,6 +120,7 @@ class AppleTvMediaPlayer(
|
||||
"""Initialize the Apple TV media player."""
|
||||
super().__init__(name, identifier, manager)
|
||||
self._playing: Playing | None = None
|
||||
self._playing_last_updated: datetime | None = None
|
||||
self._app_list: dict[str, str] = {}
|
||||
|
||||
@callback
|
||||
@@ -209,6 +210,7 @@ class AppleTvMediaPlayer(
|
||||
This is a callback function from pyatv.interface.PushListener.
|
||||
"""
|
||||
self._playing = playstatus
|
||||
self._playing_last_updated = dt_util.utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@@ -316,7 +318,7 @@ class AppleTvMediaPlayer(
|
||||
def media_position_updated_at(self) -> datetime | None:
|
||||
"""Last valid time of media position."""
|
||||
if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
|
||||
return dt_util.utcnow()
|
||||
return self._playing_last_updated
|
||||
return None
|
||||
|
||||
async def async_play_media(
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.8.1"]
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
config_entry: ApSystemsConfigEntry
|
||||
device_version: str
|
||||
battery_system: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
self.device_version = device_info.devVer
|
||||
self.battery_system = device_info.isBatterySystem
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
try:
|
||||
|
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.4.0"]
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"off_grid_status": {
|
||||
"name": "Off grid status"
|
||||
"name": "Off-grid status"
|
||||
},
|
||||
"dc_1_short_circuit_error_status": {
|
||||
"name": "DC 1 short circuit error status"
|
||||
|
@@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
super().__init__(data)
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
||||
if data.coordinator.battery_system:
|
||||
self._attr_available = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update switch status and availability."""
|
||||
|
@@ -36,9 +36,9 @@
|
||||
"wi_fi_strength": {
|
||||
"name": "Wi-Fi strength",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"sensor": {
|
||||
"threshold": {
|
||||
"state": {
|
||||
"error": "Error",
|
||||
"error": "[%key:common::state::error%]",
|
||||
"green": "Green",
|
||||
"yellow": "Yellow",
|
||||
"red": "Red"
|
||||
|
@@ -20,9 +20,6 @@ import hass_nabucasa
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
|
||||
from homeassistant.components.tts import (
|
||||
generate_media_source_id as tts_generate_media_source_id,
|
||||
)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -92,6 +89,8 @@ KEY_ASSIST_PIPELINE: HassKey[PipelineData] = HassKey(DOMAIN)
|
||||
KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey(
|
||||
"pipeline_conversation_data"
|
||||
)
|
||||
# Number of response parts to handle before streaming the response
|
||||
STREAM_RESPONSE_CHARS = 60
|
||||
|
||||
|
||||
def validate_language(data: dict[str, Any]) -> Any:
|
||||
@@ -555,7 +554,7 @@ class PipelineRun:
|
||||
event_callback: PipelineEventCallback
|
||||
language: str = None # type: ignore[assignment]
|
||||
runner_data: Any | None = None
|
||||
intent_agent: str | None = None
|
||||
intent_agent: conversation.AgentInfo | None = None
|
||||
tts_audio_output: str | dict[str, Any] | None = None
|
||||
wake_word_settings: WakeWordSettings | None = None
|
||||
audio_settings: AudioSettings = field(default_factory=AudioSettings)
|
||||
@@ -591,6 +590,9 @@ class PipelineRun:
|
||||
_intent_agent_only = False
|
||||
"""If request should only be handled by agent, ignoring sentence triggers and local processing."""
|
||||
|
||||
_streamed_response_text = False
|
||||
"""If the conversation agent streamed response text to TTS result."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Set language for pipeline."""
|
||||
self.language = self.pipeline.language or self.hass.config.language
|
||||
@@ -652,6 +654,11 @@ class PipelineRun:
|
||||
"token": self.tts_stream.token,
|
||||
"url": self.tts_stream.url,
|
||||
"mime_type": self.tts_stream.content_type,
|
||||
"stream_response": (
|
||||
self.tts_stream.supports_streaming_input
|
||||
and self.intent_agent
|
||||
and self.intent_agent.supports_streaming
|
||||
),
|
||||
}
|
||||
|
||||
self.process_event(PipelineEvent(PipelineEventType.RUN_START, data))
|
||||
@@ -899,12 +906,12 @@ class PipelineRun:
|
||||
) -> str:
|
||||
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
|
||||
# Create a background task to prepare the conversation agent
|
||||
if self.end_stage >= PipelineStage.INTENT:
|
||||
if self.end_stage >= PipelineStage.INTENT and self.intent_agent:
|
||||
self.hass.async_create_background_task(
|
||||
conversation.async_prepare_agent(
|
||||
self.hass, self.intent_agent, self.language
|
||||
self.hass, self.intent_agent.id, self.language
|
||||
),
|
||||
f"prepare conversation agent {self.intent_agent}",
|
||||
f"prepare conversation agent {self.intent_agent.id}",
|
||||
)
|
||||
|
||||
if isinstance(self.stt_provider, stt.Provider):
|
||||
@@ -1045,7 +1052,7 @@ class PipelineRun:
|
||||
message=f"Intent recognition engine {engine} is not found",
|
||||
)
|
||||
|
||||
self.intent_agent = agent_info.id
|
||||
self.intent_agent = agent_info
|
||||
|
||||
async def recognize_intent(
|
||||
self,
|
||||
@@ -1078,7 +1085,7 @@ class PipelineRun:
|
||||
PipelineEvent(
|
||||
PipelineEventType.INTENT_START,
|
||||
{
|
||||
"engine": self.intent_agent,
|
||||
"engine": self.intent_agent.id,
|
||||
"language": input_language,
|
||||
"intent_input": intent_input,
|
||||
"conversation_id": conversation_id,
|
||||
@@ -1095,11 +1102,11 @@ class PipelineRun:
|
||||
conversation_id=conversation_id,
|
||||
device_id=device_id,
|
||||
language=input_language,
|
||||
agent_id=self.intent_agent,
|
||||
agent_id=self.intent_agent.id,
|
||||
extra_system_prompt=conversation_extra_system_prompt,
|
||||
)
|
||||
|
||||
agent_id = self.intent_agent
|
||||
agent_id = self.intent_agent.id
|
||||
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
|
||||
intent_response: intent.IntentResponse | None = None
|
||||
if not processed_locally and not self._intent_agent_only:
|
||||
@@ -1121,7 +1128,7 @@ class PipelineRun:
|
||||
# If the LLM has API access, we filter out some sentences that are
|
||||
# interfering with LLM operation.
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent)
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent.id)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
@@ -1143,6 +1150,13 @@ class PipelineRun:
|
||||
agent_id = conversation.HOME_ASSISTANT_AGENT
|
||||
processed_locally = True
|
||||
|
||||
if self.tts_stream and self.tts_stream.supports_streaming_input:
|
||||
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
|
||||
else:
|
||||
tts_input_stream = None
|
||||
chat_log_role = None
|
||||
delta_character_count = 0
|
||||
|
||||
@callback
|
||||
def chat_log_delta_listener(
|
||||
chat_log: conversation.ChatLog, delta: dict
|
||||
@@ -1156,6 +1170,61 @@ class PipelineRun:
|
||||
},
|
||||
)
|
||||
)
|
||||
if tts_input_stream is None:
|
||||
return
|
||||
|
||||
nonlocal chat_log_role
|
||||
|
||||
if role := delta.get("role"):
|
||||
chat_log_role = role
|
||||
|
||||
# We are only interested in assistant deltas
|
||||
if chat_log_role != "assistant":
|
||||
return
|
||||
|
||||
if content := delta.get("content"):
|
||||
tts_input_stream.put_nowait(content)
|
||||
|
||||
if self._streamed_response_text:
|
||||
return
|
||||
|
||||
nonlocal delta_character_count
|
||||
|
||||
# Streamed responses are not cached. That's why we only start streaming text after
|
||||
# we have received enough characters that indicates it will be a long response
|
||||
# or if we have received text, and then a tool call.
|
||||
|
||||
# Tool call after we already received text
|
||||
start_streaming = delta_character_count > 0 and delta.get("tool_calls")
|
||||
|
||||
# Count characters in the content and test if we exceed streaming threshold
|
||||
if not start_streaming and content:
|
||||
delta_character_count += len(content)
|
||||
start_streaming = delta_character_count > STREAM_RESPONSE_CHARS
|
||||
|
||||
if not start_streaming:
|
||||
return
|
||||
|
||||
self._streamed_response_text = True
|
||||
|
||||
async def tts_input_stream_generator() -> AsyncGenerator[str]:
|
||||
"""Yield TTS input stream."""
|
||||
while (tts_input := await tts_input_stream.get()) is not None:
|
||||
yield tts_input
|
||||
|
||||
# Concatenate all existing queue items
|
||||
parts = []
|
||||
while not tts_input_stream.empty():
|
||||
parts.append(tts_input_stream.get_nowait())
|
||||
tts_input_stream.put_nowait(
|
||||
"".join(
|
||||
# At this point parts is only strings, None indicates end of queue
|
||||
cast(list[str], parts)
|
||||
)
|
||||
)
|
||||
|
||||
assert self.tts_stream is not None
|
||||
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
|
||||
|
||||
with (
|
||||
chat_session.async_get_chat_session(
|
||||
@@ -1199,6 +1268,8 @@ class PipelineRun:
|
||||
speech = conversation_result.response.speech.get("plain", {}).get(
|
||||
"speech", ""
|
||||
)
|
||||
if tts_input_stream and self._streamed_response_text:
|
||||
tts_input_stream.put_nowait(None)
|
||||
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during intent recognition")
|
||||
@@ -1276,26 +1347,11 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_stream.engine,
|
||||
language=self.tts_stream.language,
|
||||
options=self.tts_stream.options,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
|
||||
self.tts_stream.async_set_message(tts_input)
|
||||
if not self._streamed_response_text:
|
||||
self.tts_stream.async_set_message(tts_input)
|
||||
|
||||
tts_output = {
|
||||
"media_id": tts_media_id,
|
||||
"media_id": self.tts_stream.media_source_id,
|
||||
"token": self.tts_stream.token,
|
||||
"url": self.tts_stream.url,
|
||||
"mime_type": self.tts_stream.content_type,
|
||||
|
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
@@ -363,7 +362,7 @@ class AsusWrtRouter:
|
||||
"""Add a function to call when router is closed."""
|
||||
self._on_close.append(func)
|
||||
|
||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> bool:
|
||||
def update_options(self, new_options: Mapping[str, Any]) -> bool:
|
||||
"""Update router options."""
|
||||
req_reload = False
|
||||
for name, new_opt in new_options.items():
|
||||
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.7"]
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.6.0"]
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"step": {
|
||||
"validation": {
|
||||
"title": "Two factor authentication",
|
||||
"title": "Two-factor authentication",
|
||||
"data": {
|
||||
"verification_code": "Verification code"
|
||||
},
|
||||
|
@@ -4,8 +4,8 @@
|
||||
"user": {
|
||||
"description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel",
|
||||
"data": {
|
||||
"port": "RS485 or USB-RS485 Adaptor Port",
|
||||
"address": "Inverter Address"
|
||||
"port": "RS485 or USB-RS485 adaptor port",
|
||||
"address": "Inverter address"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_serial_ports": "No com ports found. Need a valid RS485 device to communicate."
|
||||
"no_serial_ports": "No com ports found. The integration needs a valid RS485 device to communicate."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"title": "Notify One-Time Password",
|
||||
"title": "Notify one-time password",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up one-time password delivered by notify component",
|
||||
|
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -27,6 +28,7 @@ from homeassistant.const import (
|
||||
CONF_MODE,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
@@ -86,11 +88,9 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .config import AutomationConfig, ValidationStatus
|
||||
from .const import (
|
||||
CONF_ACTIONS,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DEFAULT_INITIAL_STATE,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
@@ -14,11 +14,15 @@ from homeassistant.components import blueprint
|
||||
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
|
||||
from homeassistant.config import config_per_platform, config_without_domain
|
||||
from homeassistant.const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -30,14 +34,10 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.yaml.input import UndefinedSubstitution
|
||||
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_HIDE_ENTITY,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
@@ -58,34 +58,9 @@ _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
|
||||
def _backward_compat_schema(value: Any | None) -> Any:
|
||||
"""Backward compatibility for automations."""
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# `trigger` has been renamed to `triggers`
|
||||
if CONF_TRIGGER in value:
|
||||
if CONF_TRIGGERS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
|
||||
)
|
||||
value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
|
||||
|
||||
# `condition` has been renamed to `conditions`
|
||||
if CONF_CONDITION in value:
|
||||
if CONF_CONDITIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
|
||||
)
|
||||
value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
|
||||
|
||||
# `action` has been renamed to `actions`
|
||||
if CONF_ACTION in value:
|
||||
if CONF_ACTIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'action' and 'actions'. Please use 'actions' only."
|
||||
)
|
||||
value[CONF_ACTIONS] = value.pop(CONF_ACTION)
|
||||
|
||||
return value
|
||||
value = cv.renamed(CONF_TRIGGER, CONF_TRIGGERS)(value)
|
||||
value = cv.renamed(CONF_ACTION, CONF_ACTIONS)(value)
|
||||
return cv.renamed(CONF_CONDITION, CONF_CONDITIONS)(value)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
|
@@ -2,10 +2,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ACTIONS = "actions"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_TRIGGERS = "triggers"
|
||||
CONF_TRIGGER_VARIABLES = "trigger_variables"
|
||||
DOMAIN = "automation"
|
||||
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
|
||||
}
|
||||
|
82
homeassistant/components/aws_s3/__init__.py
Normal file
82
homeassistant/components/aws_s3/__init__.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""The AWS S3 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiobotocore.client import AioBaseClient as S3Client
|
||||
from aiobotocore.session import AioSession
|
||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DATA_BACKUP_AGENT_LISTENERS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
type S3ConfigEntry = ConfigEntry[S3Client]
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
"""Set up S3 from a config entry."""
|
||||
|
||||
data = cast(dict, entry.data)
|
||||
try:
|
||||
session = AioSession()
|
||||
# pylint: disable-next=unnecessary-dunder-call
|
||||
client = await session.create_client(
|
||||
"s3",
|
||||
endpoint_url=data.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
|
||||
).__aenter__()
|
||||
await client.head_bucket(Bucket=data[CONF_BUCKET])
|
||||
except ClientError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_credentials",
|
||||
) from err
|
||||
except ParamValidationError as err:
|
||||
if "Invalid bucket name" in str(err):
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except ValueError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_endpoint_url",
|
||||
) from err
|
||||
except ConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
def notify_backup_listeners() -> None:
|
||||
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
|
||||
listener()
|
||||
|
||||
entry.async_on_unload(entry.async_on_state_change(notify_backup_listeners))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
client = entry.runtime_data
|
||||
await client.__aexit__(None, None, None)
|
||||
return True
|
330
homeassistant/components/aws_s3/backup.py
Normal file
330
homeassistant/components/aws_s3/backup.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""Backup platform for the AWS S3 integration."""
|
||||
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
from time import time
|
||||
from typing import Any
|
||||
|
||||
from botocore.exceptions import BotoCoreError
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgent,
|
||||
BackupAgentError,
|
||||
BackupNotFound,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import S3ConfigEntry
|
||||
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# S3 part size requirements: 5 MiB to 5 GiB per part
|
||||
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
|
||||
# We set the threshold to 20 MiB to avoid too many parts.
|
||||
# Note that each part is allocated in the memory.
|
||||
MULTIPART_MIN_PART_SIZE_BYTES = 20 * 2**20
|
||||
|
||||
|
||||
def handle_boto_errors[T](
|
||||
func: Callable[..., Coroutine[Any, Any, T]],
|
||||
) -> Callable[..., Coroutine[Any, Any, T]]:
|
||||
"""Handle BotoCoreError exceptions by converting them to BackupAgentError."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
||||
"""Catch BotoCoreError and raise BackupAgentError."""
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except BotoCoreError as err:
|
||||
error_msg = f"Failed during {func.__name__}"
|
||||
raise BackupAgentError(error_msg) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def async_get_backup_agents(
|
||||
hass: HomeAssistant,
|
||||
) -> list[BackupAgent]:
|
||||
"""Return a list of backup agents."""
|
||||
entries: list[S3ConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
return [S3BackupAgent(hass, entry) for entry in entries]
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_backup_agents_listener(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
listener: Callable[[], None],
|
||||
**kwargs: Any,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a listener to be called when agents are added or removed.
|
||||
|
||||
:return: A function to unregister the listener.
|
||||
"""
|
||||
hass.data.setdefault(DATA_BACKUP_AGENT_LISTENERS, []).append(listener)
|
||||
|
||||
@callback
|
||||
def remove_listener() -> None:
|
||||
"""Remove the listener."""
|
||||
hass.data[DATA_BACKUP_AGENT_LISTENERS].remove(listener)
|
||||
if not hass.data[DATA_BACKUP_AGENT_LISTENERS]:
|
||||
del hass.data[DATA_BACKUP_AGENT_LISTENERS]
|
||||
|
||||
return remove_listener
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
base_name = suggested_filename(backup).rsplit(".", 1)[0]
|
||||
return f"{base_name}.tar", f"{base_name}.metadata.json"
|
||||
|
||||
|
||||
class S3BackupAgent(BackupAgent):
|
||||
"""Backup agent for the S3 integration."""
|
||||
|
||||
domain = DOMAIN
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: S3ConfigEntry) -> None:
|
||||
"""Initialize the S3 agent."""
|
||||
super().__init__()
|
||||
self._client = entry.runtime_data
|
||||
self._bucket: str = entry.data[CONF_BUCKET]
|
||||
self.name = entry.title
|
||||
self.unique_id = entry.entry_id
|
||||
self._backup_cache: dict[str, AgentBackup] = {}
|
||||
self._cache_expiration = time()
|
||||
|
||||
@handle_boto_errors
|
||||
async def async_download_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[bytes]:
|
||||
"""Download a backup file.
|
||||
|
||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||
:return: An async iterator that yields bytes.
|
||||
"""
|
||||
backup = await self._find_backup_by_id(backup_id)
|
||||
tar_filename, _ = suggested_filenames(backup)
|
||||
|
||||
response = await self._client.get_object(Bucket=self._bucket, Key=tar_filename)
|
||||
return response["Body"].iter_chunks()
|
||||
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
backup: AgentBackup,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup.
|
||||
|
||||
:param open_stream: A function returning an async iterator that yields bytes.
|
||||
:param backup: Metadata about the backup that should be uploaded.
|
||||
"""
|
||||
tar_filename, metadata_filename = suggested_filenames(backup)
|
||||
|
||||
try:
|
||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
await self._upload_simple(tar_filename, open_stream)
|
||||
else:
|
||||
await self._upload_multipart(tar_filename, open_stream)
|
||||
|
||||
# Upload the metadata file
|
||||
metadata_content = json.dumps(backup.as_dict())
|
||||
await self._client.put_object(
|
||||
Bucket=self._bucket,
|
||||
Key=metadata_filename,
|
||||
Body=metadata_content,
|
||||
)
|
||||
except BotoCoreError as err:
|
||||
raise BackupAgentError("Failed to upload backup") from err
|
||||
else:
|
||||
# Reset cache after successful upload
|
||||
self._cache_expiration = time()
|
||||
|
||||
async def _upload_simple(
|
||||
self,
|
||||
tar_filename: str,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
) -> None:
|
||||
"""Upload a small file using simple upload.
|
||||
|
||||
:param tar_filename: The target filename for the backup.
|
||||
:param open_stream: A function returning an async iterator that yields bytes.
|
||||
"""
|
||||
_LOGGER.debug("Starting simple upload for %s", tar_filename)
|
||||
stream = await open_stream()
|
||||
file_data = bytearray()
|
||||
async for chunk in stream:
|
||||
file_data.extend(chunk)
|
||||
|
||||
await self._client.put_object(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
Body=bytes(file_data),
|
||||
)
|
||||
|
||||
async def _upload_multipart(
|
||||
self,
|
||||
tar_filename: str,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
):
|
||||
"""Upload a large file using multipart upload.
|
||||
|
||||
:param tar_filename: The target filename for the backup.
|
||||
:param open_stream: A function returning an async iterator that yields bytes.
|
||||
"""
|
||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||
multipart_upload = await self._client.create_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
)
|
||||
upload_id = multipart_upload["UploadId"]
|
||||
try:
|
||||
parts = []
|
||||
part_number = 1
|
||||
buffer_size = 0 # bytes
|
||||
buffer: list[bytes] = []
|
||||
|
||||
stream = await open_stream()
|
||||
async for chunk in stream:
|
||||
buffer_size += len(chunk)
|
||||
buffer.append(chunk)
|
||||
|
||||
# If buffer size meets minimum part size, upload it as a part
|
||||
if buffer_size >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
_LOGGER.debug(
|
||||
"Uploading part number %d, size %d", part_number, buffer_size
|
||||
)
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=b"".join(buffer),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
part_number += 1
|
||||
buffer_size = 0
|
||||
buffer = []
|
||||
|
||||
# Upload the final buffer as the last part (no minimum size requirement)
|
||||
if buffer:
|
||||
_LOGGER.debug(
|
||||
"Uploading final part number %d, size %d", part_number, buffer_size
|
||||
)
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=b"".join(buffer),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
|
||||
await self._client.complete_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
UploadId=upload_id,
|
||||
MultipartUpload={"Parts": parts},
|
||||
)
|
||||
|
||||
except BotoCoreError:
|
||||
try:
|
||||
await self._client.abort_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
Key=tar_filename,
|
||||
UploadId=upload_id,
|
||||
)
|
||||
except BotoCoreError:
|
||||
_LOGGER.exception("Failed to abort multipart upload")
|
||||
raise
|
||||
|
||||
@handle_boto_errors
|
||||
async def async_delete_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Delete a backup file.
|
||||
|
||||
:param backup_id: The ID of the backup that was returned in async_list_backups.
|
||||
"""
|
||||
backup = await self._find_backup_by_id(backup_id)
|
||||
tar_filename, metadata_filename = suggested_filenames(backup)
|
||||
|
||||
# Delete both the backup file and its metadata file
|
||||
await self._client.delete_object(Bucket=self._bucket, Key=tar_filename)
|
||||
await self._client.delete_object(Bucket=self._bucket, Key=metadata_filename)
|
||||
|
||||
# Reset cache after successful deletion
|
||||
self._cache_expiration = time()
|
||||
|
||||
@handle_boto_errors
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||
"""List backups."""
|
||||
backups = await self._list_backups()
|
||||
return list(backups.values())
|
||||
|
||||
@handle_boto_errors
|
||||
async def async_get_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AgentBackup:
|
||||
"""Return a backup."""
|
||||
return await self._find_backup_by_id(backup_id)
|
||||
|
||||
async def _find_backup_by_id(self, backup_id: str) -> AgentBackup:
|
||||
"""Find a backup by its backup ID."""
|
||||
backups = await self._list_backups()
|
||||
if backup := backups.get(backup_id):
|
||||
return backup
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
async def _list_backups(self) -> dict[str, AgentBackup]:
|
||||
"""List backups, using a cache if possible."""
|
||||
if time() <= self._cache_expiration:
|
||||
return self._backup_cache
|
||||
|
||||
backups = {}
|
||||
response = await self._client.list_objects_v2(Bucket=self._bucket)
|
||||
|
||||
# Filter for metadata files only
|
||||
metadata_files = [
|
||||
obj
|
||||
for obj in response.get("Contents", [])
|
||||
if obj["Key"].endswith(".metadata.json")
|
||||
]
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
# Download and parse metadata file
|
||||
metadata_response = await self._client.get_object(
|
||||
Bucket=self._bucket, Key=metadata_file["Key"]
|
||||
)
|
||||
metadata_content = await metadata_response["Body"].read()
|
||||
metadata_json = json.loads(metadata_content)
|
||||
except (BotoCoreError, json.JSONDecodeError) as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to process metadata file %s: %s",
|
||||
metadata_file["Key"],
|
||||
err,
|
||||
)
|
||||
continue
|
||||
backup = AgentBackup.from_dict(metadata_json)
|
||||
backups[backup.backup_id] = backup
|
||||
|
||||
self._backup_cache = backups
|
||||
self._cache_expiration = time() + CACHE_TTL
|
||||
|
||||
return self._backup_cache
|
101
homeassistant/components/aws_s3/config_flow.py
Normal file
101
homeassistant/components/aws_s3/config_flow.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Config flow for the AWS S3 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiobotocore.session import AioSession
|
||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
AWS_DOMAIN,
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
CONF_SECRET_ACCESS_KEY,
|
||||
DEFAULT_ENDPOINT_URL,
|
||||
DESCRIPTION_AWS_S3_DOCS_URL,
|
||||
DESCRIPTION_BOTO3_DOCS_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ACCESS_KEY_ID): cv.string,
|
||||
vol.Required(CONF_SECRET_ACCESS_KEY): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(CONF_BUCKET): cv.string,
|
||||
vol.Required(CONF_ENDPOINT_URL, default=DEFAULT_ENDPOINT_URL): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.URL)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class S3ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_BUCKET: user_input[CONF_BUCKET],
|
||||
CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL],
|
||||
}
|
||||
)
|
||||
|
||||
if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith(
|
||||
AWS_DOMAIN
|
||||
):
|
||||
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
|
||||
else:
|
||||
try:
|
||||
session = AioSession()
|
||||
async with session.create_client(
|
||||
"s3",
|
||||
endpoint_url=user_input.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=user_input[CONF_ACCESS_KEY_ID],
|
||||
) as client:
|
||||
await client.head_bucket(Bucket=user_input[CONF_BUCKET])
|
||||
except ClientError:
|
||||
errors["base"] = "invalid_credentials"
|
||||
except ParamValidationError as err:
|
||||
if "Invalid bucket name" in str(err):
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except ValueError:
|
||||
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
|
||||
except ConnectionError:
|
||||
errors[CONF_ENDPOINT_URL] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_BUCKET], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"aws_s3_docs_url": DESCRIPTION_AWS_S3_DOCS_URL,
|
||||
"boto3_docs_url": DESCRIPTION_BOTO3_DOCS_URL,
|
||||
},
|
||||
)
|
23
homeassistant/components/aws_s3/const.py
Normal file
23
homeassistant/components/aws_s3/const.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Constants for the AWS S3 integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN: Final = "aws_s3"
|
||||
|
||||
CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
|
||||
AWS_DOMAIN = "amazonaws.com"
|
||||
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
)
|
||||
|
||||
DESCRIPTION_AWS_S3_DOCS_URL = "https://docs.aws.amazon.com/general/latest/gr/s3.html"
|
||||
DESCRIPTION_BOTO3_DOCS_URL = "https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html"
|
12
homeassistant/components/aws_s3/manifest.json
Normal file
12
homeassistant/components/aws_s3/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "aws_s3",
|
||||
"name": "AWS S3",
|
||||
"codeowners": ["@tomasbedrich"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aws_s3",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiobotocore==2.21.1"]
|
||||
}
|
112
homeassistant/components/aws_s3/quality_scale.yaml
Normal file
112
homeassistant/components/aws_s3/quality_scale.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: This integration does not poll.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: This integration does not have any custom actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
has-entity-name:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: This integration does not have an options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: This integration does not poll.
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: S3 is a cloud service that is not discovered on the network.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: S3 is a cloud service that is not discovered on the network.
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: This integration does not poll.
|
||||
docs-examples:
|
||||
status: exempt
|
||||
comment: The integration extends core functionality and does not require examples.
|
||||
docs-known-limitations:
|
||||
status: exempt
|
||||
comment: No known limitations.
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: This integration does not support physical devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting:
|
||||
status: exempt
|
||||
comment: There are no more detailed troubleshooting instructions available than what is already included in strings.json.
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: This integration does not have devices.
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: This integration does not have entities.
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: This integration does not use icons.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: There are no issues which can be repaired.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: This integration does not have devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
41
homeassistant/components/aws_s3/strings.json
Normal file
41
homeassistant/components/aws_s3/strings.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"access_key_id": "Access key ID",
|
||||
"secret_access_key": "Secret access key",
|
||||
"bucket": "Bucket name",
|
||||
"endpoint_url": "Endpoint URL"
|
||||
},
|
||||
"data_description": {
|
||||
"access_key_id": "Access key ID to connect to AWS S3 API",
|
||||
"secret_access_key": "Secret access key to connect to AWS S3 API",
|
||||
"bucket": "Bucket must already exist and be writable by the provided credentials.",
|
||||
"endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs."
|
||||
},
|
||||
"title": "Add AWS S3 bucket"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]",
|
||||
"invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]",
|
||||
"invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to endpoint"
|
||||
},
|
||||
"invalid_bucket_name": {
|
||||
"message": "Invalid bucket name"
|
||||
},
|
||||
"invalid_credentials": {
|
||||
"message": "Bucket cannot be accessed using provided combination of access key ID and secret access key."
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -48,7 +47,7 @@ from .const import (
|
||||
CONF_VIDEO_SOURCE,
|
||||
DEFAULT_STREAM_PROFILE,
|
||||
DEFAULT_VIDEO_SOURCE,
|
||||
DOMAIN as AXIS_DOMAIN,
|
||||
DOMAIN,
|
||||
)
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
@@ -59,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
|
||||
PROTOCOL_CHOICES = ["https", "http"]
|
||||
|
||||
|
||||
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Axis config flow."""
|
||||
|
||||
VERSION = 3
|
||||
@@ -88,7 +87,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
api = await get_axis_api(self.hass, MappingProxyType(user_input))
|
||||
api = await get_axis_api(self.hass, user_input)
|
||||
|
||||
except AuthenticationRequired:
|
||||
errors["base"] = "invalid_auth"
|
||||
@@ -147,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
model = self.config[CONF_MODEL]
|
||||
same_model = [
|
||||
entry.data[CONF_NAME]
|
||||
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
|
||||
]
|
||||
|
||||
|
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .hub import AxisHub
|
||||
@@ -61,7 +61,7 @@ class AxisEntity(Entity):
|
||||
self.hub = hub
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AXIS_DOMAIN, hub.unique_id)},
|
||||
identifiers={(DOMAIN, hub.unique_id)},
|
||||
serial_number=hub.unique_id,
|
||||
)
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Axis network device abstraction."""
|
||||
|
||||
from asyncio import timeout
|
||||
from types import MappingProxyType
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import axis
|
||||
@@ -23,7 +23,7 @@ from ..errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
async def get_axis_api(
|
||||
hass: HomeAssistant,
|
||||
config: MappingProxyType[str, Any],
|
||||
config: Mapping[str, Any],
|
||||
) -> axis.AxisDevice:
|
||||
"""Create a Axis device API."""
|
||||
session = get_async_client(hass, verify_ssl=False)
|
||||
|
@@ -3,11 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from azure.eventhub import EventData, EventDataBatch
|
||||
@@ -179,7 +178,7 @@ class AzureEventHub:
|
||||
await self.async_send(None)
|
||||
await self._queue.join()
|
||||
|
||||
def update_options(self, new_options: MappingProxyType[str, Any]) -> None:
|
||||
def update_options(self, new_options: Mapping[str, Any]) -> None:
|
||||
"""Update options."""
|
||||
self._send_interval = new_options[CONF_SEND_INTERVAL]
|
||||
|
||||
|
@@ -2,8 +2,8 @@
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from azure.core.exceptions import (
|
||||
AzureError,
|
||||
ClientAuthenticationError,
|
||||
HttpResponseError,
|
||||
ResourceNotFoundError,
|
||||
)
|
||||
from azure.core.pipeline.transport._aiohttp import (
|
||||
@@ -39,11 +39,20 @@ async def async_setup_entry(
|
||||
session = async_create_clientsession(
|
||||
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
|
||||
)
|
||||
container_client = ContainerClient(
|
||||
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
||||
container_name=entry.data[CONF_CONTAINER_NAME],
|
||||
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=session),
|
||||
|
||||
def create_container_client() -> ContainerClient:
|
||||
"""Create a ContainerClient."""
|
||||
|
||||
return ContainerClient(
|
||||
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
||||
container_name=entry.data[CONF_CONTAINER_NAME],
|
||||
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=session),
|
||||
)
|
||||
|
||||
# has a blocking call to open in cpython
|
||||
container_client: ContainerClient = await hass.async_add_executor_job(
|
||||
create_container_client
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -61,7 +70,7 @@ async def async_setup_entry(
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]},
|
||||
) from err
|
||||
except HttpResponseError as err:
|
||||
except AzureError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
|
@@ -8,7 +8,7 @@ import json
|
||||
import logging
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from azure.core.exceptions import HttpResponseError
|
||||
from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError
|
||||
from azure.storage.blob import BlobProperties
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
@@ -80,6 +80,20 @@ def handle_backup_errors[_R, **P](
|
||||
f"Error during backup operation in {func.__name__}:"
|
||||
f" Status {err.status_code}, message: {err.message}"
|
||||
) from err
|
||||
except ServiceRequestError as err:
|
||||
raise BackupAgentError(
|
||||
f"Timeout during backup operation in {func.__name__}"
|
||||
) from err
|
||||
except AzureError as err:
|
||||
_LOGGER.debug(
|
||||
"Error during backup in %s: %s",
|
||||
func.__name__,
|
||||
err,
|
||||
exc_info=True,
|
||||
)
|
||||
raise BackupAgentError(
|
||||
f"Error during backup operation in {func.__name__}: {err}"
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
@@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for azure storage."""
|
||||
|
||||
def get_account_url(self, account_name: str) -> str:
|
||||
"""Get the account URL."""
|
||||
return f"https://{account_name}.blob.core.windows.net/"
|
||||
async def get_container_client(
|
||||
self, account_name: str, container_name: str, storage_account_key: str
|
||||
) -> ContainerClient:
|
||||
"""Get the container client.
|
||||
|
||||
ContainerClient has a blocking call to open in cpython
|
||||
"""
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
def create_container_client() -> ContainerClient:
|
||||
return ContainerClient(
|
||||
account_url=f"https://{account_name}.blob.core.windows.net/",
|
||||
container_name=container_name,
|
||||
credential=storage_account_key,
|
||||
transport=AioHttpTransport(session=session),
|
||||
)
|
||||
|
||||
return await self.hass.async_add_executor_job(create_container_client)
|
||||
|
||||
async def validate_config(
|
||||
self, container_client: ContainerClient
|
||||
@@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match(
|
||||
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
|
||||
)
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=user_input[CONF_ACCOUNT_NAME],
|
||||
container_name=user_input[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
errors = await self.validate_config(container_client)
|
||||
|
||||
@@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
|
||||
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
|
||||
errors = await self.validate_config(container_client)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
@@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(
|
||||
reconfigure_entry.data[CONF_ACCOUNT_NAME]
|
||||
),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
|
||||
container_name=user_input[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
errors = await self.validate_config(container_client)
|
||||
if not errors:
|
||||
|
@@ -23,6 +23,7 @@ from .const import DATA_MANAGER, DOMAIN
|
||||
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
|
||||
from .http import async_register_http_views
|
||||
from .manager import (
|
||||
AddonErrorData,
|
||||
BackupManager,
|
||||
BackupManagerError,
|
||||
BackupPlatformEvent,
|
||||
@@ -48,6 +49,7 @@ from .util import suggested_filename, suggested_filename_from_name_date
|
||||
from .websocket import async_register_websocket_handlers
|
||||
|
||||
__all__ = [
|
||||
"AddonErrorData",
|
||||
"AddonInfo",
|
||||
"AgentBackup",
|
||||
"BackupAgent",
|
||||
@@ -79,7 +81,7 @@ __all__ = [
|
||||
"suggested_filename_from_name_date",
|
||||
]
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
PLATFORMS = [Platform.EVENT, Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user