mirror of
https://github.com/home-assistant/core.git
synced 2025-10-14 06:09:39 +00:00
Compare commits
884 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79b1c3f573 | ||
![]() |
46dd245560 | ||
![]() |
bbf36d1a36 | ||
![]() |
ad74d42b15 | ||
![]() |
953963c95b | ||
![]() |
650f2babf9 | ||
![]() |
979dafd0a7 | ||
![]() |
24b8545ebe | ||
![]() |
bf8bfa6321 | ||
![]() |
7fe5fee124 | ||
![]() |
3e7ada2056 | ||
![]() |
7ad75493ff | ||
![]() |
71048545fa | ||
![]() |
49f59007df | ||
![]() |
80b0c10a38 | ||
![]() |
81b95f4050 | ||
![]() |
c9380d4972 | ||
![]() |
c505bf2df2 | ||
![]() |
dfd956b083 | ||
![]() |
a50cf1d00a | ||
![]() |
6617d676e5 | ||
![]() |
9f12226b2b | ||
![]() |
d8d48b0a21 | ||
![]() |
0cd8dce9df | ||
![]() |
34751fcd86 | ||
![]() |
b003d9675c | ||
![]() |
98dd56eed5 | ||
![]() |
66e490e55f | ||
![]() |
0b32caa71f | ||
![]() |
9b46796969 | ||
![]() |
8409385fca | ||
![]() |
27859a2784 | ||
![]() |
28f78c0c67 | ||
![]() |
a3429848a2 | ||
![]() |
757d05a74e | ||
![]() |
253950f84f | ||
![]() |
af36a67b89 | ||
![]() |
b784cc011d | ||
![]() |
7c0a933452 | ||
![]() |
8b207df819 | ||
![]() |
d297be2698 | ||
![]() |
de67135e86 | ||
![]() |
b684007fbb | ||
![]() |
98347345d1 | ||
![]() |
c0302e6eca | ||
![]() |
06bc98a3a2 | ||
![]() |
32858bcea3 | ||
![]() |
979260f4be | ||
![]() |
408b52de1b | ||
![]() |
b81b57cdf7 | ||
![]() |
f4c3f5d074 | ||
![]() |
abe3e3094e | ||
![]() |
53d6f4948e | ||
![]() |
1b09c65e74 | ||
![]() |
21649244e9 | ||
![]() |
741c702ff3 | ||
![]() |
cf498b7beb | ||
![]() |
f24fe9c246 | ||
![]() |
d37a5cdde5 | ||
![]() |
a3174a88f5 | ||
![]() |
c3724186cf | ||
![]() |
633f9b2f01 | ||
![]() |
1d7f3416d3 | ||
![]() |
44fefb3216 | ||
![]() |
e938dcfbda | ||
![]() |
20d5d3c162 | ||
![]() |
b12566e265 | ||
![]() |
b430496b13 | ||
![]() |
aed98a830f | ||
![]() |
e65235b207 | ||
![]() |
bf72e3c965 | ||
![]() |
b07d09f7dc | ||
![]() |
41cd90648e | ||
![]() |
e852a2eb9b | ||
![]() |
c62345b9a3 | ||
![]() |
69d1faea35 | ||
![]() |
706eb8f36a | ||
![]() |
ddfbeffd28 | ||
![]() |
d8c2732bcb | ||
![]() |
5b3d094a1e | ||
![]() |
d119c96aee | ||
![]() |
d4111617ca | ||
![]() |
f8d547f2d3 | ||
![]() |
36dde3ff68 | ||
![]() |
209f9b6722 | ||
![]() |
f5cbae0cd5 | ||
![]() |
9ecaa10e51 | ||
![]() |
5cd6370822 | ||
![]() |
ea17d36cbb | ||
![]() |
c3f63e0739 | ||
![]() |
7d77fa92c2 | ||
![]() |
ac0dbb17af | ||
![]() |
7e280e2b27 | ||
![]() |
515ad6164d | ||
![]() |
f0916aeb86 | ||
![]() |
22f17a1887 | ||
![]() |
81914258e9 | ||
![]() |
ec1df9b427 | ||
![]() |
fb484e87c0 | ||
![]() |
5d1d113a25 | ||
![]() |
738d3a13e1 | ||
![]() |
2c58d860b6 | ||
![]() |
d0e26c3dee | ||
![]() |
6eca0b2a39 | ||
![]() |
502f0cd2ce | ||
![]() |
114fbb1278 | ||
![]() |
5d26f5d01d | ||
![]() |
0d58048cea | ||
![]() |
39d5fb82e5 | ||
![]() |
bfbb2826bc | ||
![]() |
aabf0dab88 | ||
![]() |
33eaf081cd | ||
![]() |
95e72b4c4b | ||
![]() |
9e8e5c37f4 | ||
![]() |
1a64108eea | ||
![]() |
ed3f25489e | ||
![]() |
4aaf7c5432 | ||
![]() |
cd3e045e06 | ||
![]() |
b152d59afe | ||
![]() |
9ac1475251 | ||
![]() |
8ed1a29c82 | ||
![]() |
ccbc3b5e39 | ||
![]() |
dbcd5f4c2c | ||
![]() |
34c2579507 | ||
![]() |
eb6fda8387 | ||
![]() |
d8ec1d36b2 | ||
![]() |
aaa073183c | ||
![]() |
8e71559068 | ||
![]() |
87f6b7cdd4 | ||
![]() |
d3b47b6d89 | ||
![]() |
f32f73a7d1 | ||
![]() |
b63f882c3f | ||
![]() |
548e956670 | ||
![]() |
5c28919fba | ||
![]() |
6fe54e31cc | ||
![]() |
e9440c49d5 | ||
![]() |
53844488d8 | ||
![]() |
16a947aa5f | ||
![]() |
c6ab2c5d0a | ||
![]() |
6826a8829c | ||
![]() |
f16090caf4 | ||
![]() |
401dd4a32a | ||
![]() |
b7318b1914 | ||
![]() |
f9ce3f3bc4 | ||
![]() |
e5a081c7dd | ||
![]() |
690579749e | ||
![]() |
4d0c4ac5b3 | ||
![]() |
0bcc8624b7 | ||
![]() |
0bf772b68b | ||
![]() |
8a2b34cc09 | ||
![]() |
1e9bc278e0 | ||
![]() |
0fbdb47dcf | ||
![]() |
366354c90c | ||
![]() |
2e72216a1c | ||
![]() |
63e55bff52 | ||
![]() |
8a8289b1a4 | ||
![]() |
0db8140c13 | ||
![]() |
52939865fa | ||
![]() |
e61da2fff3 | ||
![]() |
5255bf20d3 | ||
![]() |
91271f388c | ||
![]() |
b187b17a4f | ||
![]() |
383db60dff | ||
![]() |
67b18aef5b | ||
![]() |
405c1cdc86 | ||
![]() |
c260b7fa16 | ||
![]() |
804cae02ee | ||
![]() |
609bd6313a | ||
![]() |
67038c6ba8 | ||
![]() |
b45a952d61 | ||
![]() |
e379bfe383 | ||
![]() |
5f36023b9e | ||
![]() |
82739bac3b | ||
![]() |
69a8ba2af8 | ||
![]() |
ef254a1c3d | ||
![]() |
511da9557b | ||
![]() |
bcd604eec2 | ||
![]() |
08fa701854 | ||
![]() |
748ce05341 | ||
![]() |
a3310330f4 | ||
![]() |
aa39dede59 | ||
![]() |
155a5f7c26 | ||
![]() |
af6a4bb6cf | ||
![]() |
872140123d | ||
![]() |
3c7f729b15 | ||
![]() |
81af0e2ada | ||
![]() |
9be06d8be4 | ||
![]() |
2da64b03f3 | ||
![]() |
3ede54def9 | ||
![]() |
3b56e75e1d | ||
![]() |
8f77cc2f7b | ||
![]() |
cdb283319d | ||
![]() |
a30b5690f8 | ||
![]() |
aac65416ff | ||
![]() |
af87168ca3 | ||
![]() |
251a1d6619 | ||
![]() |
fd65ce08c1 | ||
![]() |
62477a3457 | ||
![]() |
a077c280c8 | ||
![]() |
fb3049d6a2 | ||
![]() |
fb420d5952 | ||
![]() |
c1de781a23 | ||
![]() |
05f93f27df | ||
![]() |
99c815bcbd | ||
![]() |
d60c52bbc7 | ||
![]() |
9964bd40ed | ||
![]() |
317bd8ffd2 | ||
![]() |
572d5a09cd | ||
![]() |
368116d242 | ||
![]() |
550f9ba9b0 | ||
![]() |
b067e1deee | ||
![]() |
aa8f3ad307 | ||
![]() |
03ce1f46f6 | ||
![]() |
4343e84ecf | ||
![]() |
80890f0096 | ||
![]() |
a58e428f48 | ||
![]() |
adc88deaa8 | ||
![]() |
ad025f0b3b | ||
![]() |
90c308a393 | ||
![]() |
6fd756299e | ||
![]() |
00182e7b9f | ||
![]() |
3871688dfb | ||
![]() |
4c2b12d8f1 | ||
![]() |
af7bf428d8 | ||
![]() |
cbccf011e7 | ||
![]() |
2b37cbe079 | ||
![]() |
d0983b69df | ||
![]() |
d0632ab636 | ||
![]() |
aeb0504c1c | ||
![]() |
621c375a25 | ||
![]() |
1499c0a563 | ||
![]() |
09ee52c4cb | ||
![]() |
663acfe89f | ||
![]() |
07d7191db3 | ||
![]() |
c89c0d7824 | ||
![]() |
0f5d300db4 | ||
![]() |
270e87166c | ||
![]() |
50bb249a93 | ||
![]() |
3f1d25b9d0 | ||
![]() |
6b97b4f5ac | ||
![]() |
76be95d7e0 | ||
![]() |
ae7d464878 | ||
![]() |
a58e357cd5 | ||
![]() |
43c7a8a119 | ||
![]() |
7128092139 | ||
![]() |
16ccf0affd | ||
![]() |
e5872cc0e4 | ||
![]() |
27ef8a1ced | ||
![]() |
bea3d841d2 | ||
![]() |
0426b70046 | ||
![]() |
c47fd90eb3 | ||
![]() |
a773534809 | ||
![]() |
d0668d3a6c | ||
![]() |
f49ce5d1b4 | ||
![]() |
e5a2b0d2d8 | ||
![]() |
7ba9bcebed | ||
![]() |
3bd5d83c9c | ||
![]() |
0f3b7b73af | ||
![]() |
38d2410156 | ||
![]() |
82693d9dca | ||
![]() |
a88ac1e1fc | ||
![]() |
37bef01f67 | ||
![]() |
7c3e64673f | ||
![]() |
16b59220f3 | ||
![]() |
050a558243 | ||
![]() |
ddb049e884 | ||
![]() |
891640972b | ||
![]() |
2e824f3fa5 | ||
![]() |
4ca643342e | ||
![]() |
f8651d9faa | ||
![]() |
b826970b16 | ||
![]() |
ba356c41e0 | ||
![]() |
b9f97e64f8 | ||
![]() |
89599bc3cf | ||
![]() |
c989f6dc09 | ||
![]() |
706bc4c307 | ||
![]() |
c9a97da0ef | ||
![]() |
b3d46aa074 | ||
![]() |
4a28133f11 | ||
![]() |
308421f1d6 | ||
![]() |
03bfff3660 | ||
![]() |
0fb73a6332 | ||
![]() |
711b8e10a3 | ||
![]() |
9b77e16ffc | ||
![]() |
6749424508 | ||
![]() |
f6f84fc201 | ||
![]() |
34ccb6588c | ||
![]() |
2088092f7c | ||
![]() |
0067b6a84d | ||
![]() |
80c108c25a | ||
![]() |
53545c984b | ||
![]() |
01fd33f173 | ||
![]() |
3ad59f877c | ||
![]() |
c1aaceebb6 | ||
![]() |
404b1f40d6 | ||
![]() |
3062312649 | ||
![]() |
10893f6246 | ||
![]() |
e3aa4679a0 | ||
![]() |
d79067db7e | ||
![]() |
f0dda4f875 | ||
![]() |
b56f4bd9f8 | ||
![]() |
992793295c | ||
![]() |
debb928b30 | ||
![]() |
6ca476fe21 | ||
![]() |
b64ae55c66 | ||
![]() |
8bc775dacc | ||
![]() |
988a335e9d | ||
![]() |
f7c4900d5c | ||
![]() |
c3b5bf7437 | ||
![]() |
a08cb2ca9d | ||
![]() |
a882cfafb1 | ||
![]() |
3d08601796 | ||
![]() |
63af60c7e7 | ||
![]() |
93df0a6d70 | ||
![]() |
ff3407ea25 | ||
![]() |
79fb722657 | ||
![]() |
3eb6e75d3e | ||
![]() |
6d6188e34d | ||
![]() |
fa6bd786d2 | ||
![]() |
c6ed7754c7 | ||
![]() |
b4aa6f9f8b | ||
![]() |
c71fcc8cbb | ||
![]() |
83b41897f8 | ||
![]() |
c630037f04 | ||
![]() |
5ba38e5053 | ||
![]() |
95d980da4a | ||
![]() |
7b855927e2 | ||
![]() |
b63655057c | ||
![]() |
4b3ad0a1cd | ||
![]() |
9ade1de3d5 | ||
![]() |
333c151955 | ||
![]() |
ebcee2eb35 | ||
![]() |
15b28bda14 | ||
![]() |
c03832da63 | ||
![]() |
ffcdd85117 | ||
![]() |
0b11fda017 | ||
![]() |
3832c8efd9 | ||
![]() |
6fd8a7a34e | ||
![]() |
4b2ebf5487 | ||
![]() |
045cdee30c | ||
![]() |
b76d7edf74 | ||
![]() |
146b5691e7 | ||
![]() |
fe5f8e041b | ||
![]() |
13f634fa16 | ||
![]() |
7c0d5526f3 | ||
![]() |
780376e411 | ||
![]() |
18c16c464e | ||
![]() |
30e980d389 | ||
![]() |
e3d3b87f2e | ||
![]() |
032a6f3143 | ||
![]() |
d7ecbb8ebe | ||
![]() |
ed086e5200 | ||
![]() |
08ebc4ce62 | ||
![]() |
1879183b24 | ||
![]() |
ccb77ba1e9 | ||
![]() |
7da3065de6 | ||
![]() |
813e60d31a | ||
![]() |
1b70ea0c3f | ||
![]() |
d57dbb4319 | ||
![]() |
80aebcc7d2 | ||
![]() |
76b956a969 | ||
![]() |
1aafa459e5 | ||
![]() |
e33a3bb94a | ||
![]() |
07f3d6ebd8 | ||
![]() |
c460b7abc9 | ||
![]() |
5805fbb99f | ||
![]() |
a445ebdf3c | ||
![]() |
78e53e35c2 | ||
![]() |
0c771f1c42 | ||
![]() |
eb66da6436 | ||
![]() |
3eb6a68d12 | ||
![]() |
cd5f6a0c56 | ||
![]() |
ab6d6ac1ce | ||
![]() |
573134fcb4 | ||
![]() |
5ecb3f9be2 | ||
![]() |
ab4687d914 | ||
![]() |
b859be8cea | ||
![]() |
187a20288b | ||
![]() |
7e664fbb3b | ||
![]() |
91799e2b52 | ||
![]() |
98bcf4f28d | ||
![]() |
a6fb9e6100 | ||
![]() |
f352c51990 | ||
![]() |
460bd2b3bb | ||
![]() |
ec690bb369 | ||
![]() |
16dae8457a | ||
![]() |
38599d2970 | ||
![]() |
5013b7e049 | ||
![]() |
b21c81656f | ||
![]() |
69a5c63b71 | ||
![]() |
9b854bdcd3 | ||
![]() |
f335127750 | ||
![]() |
9131f5fa69 | ||
![]() |
976d375a33 | ||
![]() |
094f7ee718 | ||
![]() |
a7cf76491b | ||
![]() |
58a9142f42 | ||
![]() |
f6df85f8ce | ||
![]() |
4679e670f1 | ||
![]() |
f3039f96ec | ||
![]() |
4ec71c58bd | ||
![]() |
8bce9be590 | ||
![]() |
49bbdb3c21 | ||
![]() |
e60b975b10 | ||
![]() |
a87c29b5d9 | ||
![]() |
0a982f6fab | ||
![]() |
0f8b934e68 | ||
![]() |
235298a1b2 | ||
![]() |
cf3f755edc | ||
![]() |
0cc1a17d04 | ||
![]() |
f5fa7e4400 | ||
![]() |
d08d00daa7 | ||
![]() |
a015e551eb | ||
![]() |
bcabf6da91 | ||
![]() |
3498882fe1 | ||
![]() |
52f3238e17 | ||
![]() |
dc8bfb76dc | ||
![]() |
0a0b60566d | ||
![]() |
96d0ee3153 | ||
![]() |
20c66b1fa3 | ||
![]() |
3c260c91c8 | ||
![]() |
247bc6f673 | ||
![]() |
b9d957837e | ||
![]() |
f8d11c843d | ||
![]() |
431045f036 | ||
![]() |
10786bbe7f | ||
![]() |
352c572e5d | ||
![]() |
0edd7302d5 | ||
![]() |
06e977b444 | ||
![]() |
01ba578016 | ||
![]() |
cce95312a9 | ||
![]() |
a7be7bcd0a | ||
![]() |
a8e86a62a4 | ||
![]() |
c1ec8971ae | ||
![]() |
7746ecc9fb | ||
![]() |
24289d5dbb | ||
![]() |
38210ebbc6 | ||
![]() |
b78f163bb0 | ||
![]() |
86c27b50f1 | ||
![]() |
333dccc7af | ||
![]() |
61475d0a0c | ||
![]() |
4d17b18761 | ||
![]() |
2f46a81e3e | ||
![]() |
fae47358b8 | ||
![]() |
4c052643ca | ||
![]() |
b7a071b23f | ||
![]() |
f0a8e8ea04 | ||
![]() |
caf306799b | ||
![]() |
d9a2cc93ba | ||
![]() |
dbdd4f0e39 | ||
![]() |
edc44230b4 | ||
![]() |
4d7a468c0e | ||
![]() |
a06595c08d | ||
![]() |
ff13b4c6b3 | ||
![]() |
8a755e790f | ||
![]() |
0f43476d03 | ||
![]() |
856f8fd6de | ||
![]() |
bba47ad9b1 | ||
![]() |
79f131066c | ||
![]() |
ac237ee10f | ||
![]() |
11debb1568 | ||
![]() |
b0942d86fe | ||
![]() |
12510b0c97 | ||
![]() |
cc7bed5dab | ||
![]() |
b0e2f5f375 | ||
![]() |
89a9634d35 | ||
![]() |
0f72008090 | ||
![]() |
7ef33a7219 | ||
![]() |
b96ce9c210 | ||
![]() |
7b4df98875 | ||
![]() |
5badbcb012 | ||
![]() |
ca265966e7 | ||
![]() |
af5374d38b | ||
![]() |
76fa581bb9 | ||
![]() |
4a374f0378 | ||
![]() |
15165a3c93 | ||
![]() |
584ce043e5 | ||
![]() |
b0df223f5a | ||
![]() |
201dab93ff | ||
![]() |
10f296ba17 | ||
![]() |
464f17f182 | ||
![]() |
a63a11a11a | ||
![]() |
c1194c90cb | ||
![]() |
4acc6f333e | ||
![]() |
07aba74757 | ||
![]() |
4fd27e879e | ||
![]() |
ec13eecc59 | ||
![]() |
e48bcd2070 | ||
![]() |
d454f85572 | ||
![]() |
6a6dfdff4d | ||
![]() |
b9c233f013 | ||
![]() |
7418011d6d | ||
![]() |
3a6a439c02 | ||
![]() |
34c4dc2e80 | ||
![]() |
56853787e7 | ||
![]() |
976cbdd2aa | ||
![]() |
4e10ca3214 | ||
![]() |
50dd6b69cd | ||
![]() |
8a9b19f327 | ||
![]() |
680f8f8d5a | ||
![]() |
fe5bf96e5d | ||
![]() |
7d74b74570 | ||
![]() |
a4501b93c4 | ||
![]() |
39a5f68914 | ||
![]() |
3a2d4ac7fa | ||
![]() |
76db2b39b0 | ||
![]() |
fe1a7f6d69 | ||
![]() |
0eaa6045c1 | ||
![]() |
40573bf393 | ||
![]() |
748f1c3607 | ||
![]() |
7fdb76cdf2 | ||
![]() |
1508d7a3ac | ||
![]() |
0698ae4850 | ||
![]() |
654159d34d | ||
![]() |
9c90aaf830 | ||
![]() |
5cbf77221a | ||
![]() |
508afd3b90 | ||
![]() |
7c63cfbbc7 | ||
![]() |
f90f4629ab | ||
![]() |
1e4c94b69c | ||
![]() |
67868220eb | ||
![]() |
56907392d3 | ||
![]() |
30df871787 | ||
![]() |
fd1a8dd96c | ||
![]() |
f42eb0d5ca | ||
![]() |
98a388e65a | ||
![]() |
6d9fa34f65 | ||
![]() |
2dd0a182ab | ||
![]() |
15773cb3e0 | ||
![]() |
4aedafc73a | ||
![]() |
25b093e69e | ||
![]() |
140fc48ede | ||
![]() |
fea5d007fc | ||
![]() |
c423501804 | ||
![]() |
9137957cf9 | ||
![]() |
f4528d0db2 | ||
![]() |
5bc6ed4cef | ||
![]() |
7968cd650a | ||
![]() |
cbb76be9d0 | ||
![]() |
9f855c7d01 | ||
![]() |
bd42827d43 | ||
![]() |
47220ae4a6 | ||
![]() |
e0367dc721 | ||
![]() |
35dc5ba742 | ||
![]() |
f7325a7d35 | ||
![]() |
a798b508bc | ||
![]() |
255d706c24 | ||
![]() |
cc8e0ef942 | ||
![]() |
d9a3b04e30 | ||
![]() |
4a65bed0eb | ||
![]() |
02adcc532f | ||
![]() |
fbd5ccf156 | ||
![]() |
8d69a4968f | ||
![]() |
e3b90ea3f7 | ||
![]() |
6c7355785a | ||
![]() |
a1ac1fb091 | ||
![]() |
8ca5a04a5d | ||
![]() |
04f75d6557 | ||
![]() |
26f09bae28 | ||
![]() |
2af961513d | ||
![]() |
fd1edf1bb6 | ||
![]() |
152a80abed | ||
![]() |
c289f9f099 | ||
![]() |
ed89d4869d | ||
![]() |
5b4187a9eb | ||
![]() |
5b79c1f9ef | ||
![]() |
b61b0321d6 | ||
![]() |
bb17cbdd83 | ||
![]() |
91e0395c1c | ||
![]() |
a71e0a4b29 | ||
![]() |
4e77969f5e | ||
![]() |
cf6480cda0 | ||
![]() |
1b3e5460a9 | ||
![]() |
a004e6aa68 | ||
![]() |
6610bbe7bb | ||
![]() |
2c7876fa66 | ||
![]() |
835f433cf7 | ||
![]() |
9d16edc1dc | ||
![]() |
e6d814da1d | ||
![]() |
7c5e852303 | ||
![]() |
149a3165e6 | ||
![]() |
82058f0b50 | ||
![]() |
63baf6fb0f | ||
![]() |
ad6315be5c | ||
![]() |
b4489e132c | ||
![]() |
2538cda9d4 | ||
![]() |
49a153a2e5 | ||
![]() |
d68148417f | ||
![]() |
8b21b415c4 | ||
![]() |
c28493098a | ||
![]() |
747490ab34 | ||
![]() |
ee816ed3dd | ||
![]() |
16f1ef5a44 | ||
![]() |
a2e705880d | ||
![]() |
e39997ca10 | ||
![]() |
620271c782 | ||
![]() |
d974cd4606 | ||
![]() |
247df5874b | ||
![]() |
e1060f154e | ||
![]() |
3f9e3d0905 | ||
![]() |
becc011135 | ||
![]() |
6660cf701d | ||
![]() |
280f49540e | ||
![]() |
a2e2c35011 | ||
![]() |
3a83f4bdbe | ||
![]() |
02d94f2fd0 | ||
![]() |
53a91ece4e | ||
![]() |
5446641f09 | ||
![]() |
e19c97af7c | ||
![]() |
7e696f19d3 | ||
![]() |
6906a19c01 | ||
![]() |
93f9f1b320 | ||
![]() |
3f427602ba | ||
![]() |
e25f216fd6 | ||
![]() |
b47be05efc | ||
![]() |
9d40ae96b5 | ||
![]() |
f131959f4b | ||
![]() |
0054904454 | ||
![]() |
5c8d8a290f | ||
![]() |
dcb3a57476 | ||
![]() |
78225c9ddd | ||
![]() |
215215747e | ||
![]() |
5cd85f9f00 | ||
![]() |
4674cb406e | ||
![]() |
82b77a8108 | ||
![]() |
1f9721bad3 | ||
![]() |
6aba87f3a6 | ||
![]() |
edad387b12 | ||
![]() |
f4b8a95205 | ||
![]() |
29adc6a27b | ||
![]() |
fed6625324 | ||
![]() |
1de97e3a35 | ||
![]() |
a6536bb622 | ||
![]() |
59e43ab6e4 | ||
![]() |
29f128eaad | ||
![]() |
d88efe405e | ||
![]() |
f866ff5f2b | ||
![]() |
efa0e1b2ac | ||
![]() |
4a432781ca | ||
![]() |
6c2dc521a3 | ||
![]() |
b112a742b2 | ||
![]() |
46cb9cff41 | ||
![]() |
9f65dcf4ba | ||
![]() |
13c0a59e28 | ||
![]() |
7d69b90eac | ||
![]() |
60dd94d5b0 | ||
![]() |
2196bd66c7 | ||
![]() |
8895f9b70a | ||
![]() |
a074cf4afd | ||
![]() |
2fd6431cff | ||
![]() |
5642027ffb | ||
![]() |
93272e3083 | ||
![]() |
2b5e7c2611 | ||
![]() |
683d960fa5 | ||
![]() |
d445c16697 | ||
![]() |
e4df0481da | ||
![]() |
3d1a5f76b2 | ||
![]() |
263bdaa565 | ||
![]() |
212660330f | ||
![]() |
f83d4e524b | ||
![]() |
6bffd9a892 | ||
![]() |
02e03340df | ||
![]() |
e92e26b73a | ||
![]() |
f69fc79fd1 | ||
![]() |
e7d982ee11 | ||
![]() |
94132e5572 | ||
![]() |
7920c5e5e8 | ||
![]() |
df59f87f34 | ||
![]() |
c0a2e6b655 | ||
![]() |
b4b2302e04 | ||
![]() |
098d82f4d3 | ||
![]() |
2883aacfa3 | ||
![]() |
ec440dface | ||
![]() |
3d385796da | ||
![]() |
3e14b2dc61 | ||
![]() |
7547a92479 | ||
![]() |
625c2aa19d | ||
![]() |
ec69edcb79 | ||
![]() |
f823afeadc | ||
![]() |
903db07feb | ||
![]() |
43cee39528 | ||
![]() |
d5cc3208af | ||
![]() |
94c8d74a66 | ||
![]() |
b3c851502d | ||
![]() |
a74e35795c | ||
![]() |
b464096edb | ||
![]() |
5228282f69 | ||
![]() |
fd67a079db | ||
![]() |
3ae4eba457 | ||
![]() |
dba326f16b | ||
![]() |
2b06fbbcf0 | ||
![]() |
d278dd9477 | ||
![]() |
fe03c9da68 | ||
![]() |
a6a6a7b69c | ||
![]() |
29df13abe9 | ||
![]() |
6db5ff98ed | ||
![]() |
25607c7129 | ||
![]() |
58f1d1754e | ||
![]() |
6273ad85f8 | ||
![]() |
a68af0a3a9 | ||
![]() |
f8de0594b9 | ||
![]() |
8541ae0360 | ||
![]() |
dfc345a921 | ||
![]() |
87f236c05c | ||
![]() |
0127974f09 | ||
![]() |
108082fd07 | ||
![]() |
61d6bd0cb5 | ||
![]() |
3a2138131c | ||
![]() |
ec65ebacd3 | ||
![]() |
b45e49902c | ||
![]() |
12781bf842 | ||
![]() |
d0ca8e62a4 | ||
![]() |
c37cd835b9 | ||
![]() |
3ee3ae7633 | ||
![]() |
02f174e2e6 | ||
![]() |
16cf16e418 | ||
![]() |
de12c21ce7 | ||
![]() |
b8c9f67533 | ||
![]() |
ba73ac12ba | ||
![]() |
0a219081ea | ||
![]() |
b0163b65c6 | ||
![]() |
83e3f680bf | ||
![]() |
e443dc1274 | ||
![]() |
717a21dc7b | ||
![]() |
1b6f0b78aa | ||
![]() |
e8d4a25635 | ||
![]() |
162e502962 | ||
![]() |
3cc94f7d6a | ||
![]() |
dfac9c5e03 | ||
![]() |
c96458c7e4 | ||
![]() |
9a867cbb75 | ||
![]() |
b15caf31a9 | ||
![]() |
0331ebdd47 | ||
![]() |
0b7d2aa4d7 | ||
![]() |
821de0e369 | ||
![]() |
02bcdf5162 | ||
![]() |
e7e2f4e786 | ||
![]() |
e969d364e6 | ||
![]() |
4862f6d516 | ||
![]() |
36ed3b1177 | ||
![]() |
9cc20fc6b8 | ||
![]() |
dd239661e7 | ||
![]() |
0bbb56dd05 | ||
![]() |
0bfcb99c04 | ||
![]() |
bdd255176c | ||
![]() |
20428e670b | ||
![]() |
78af0a4705 | ||
![]() |
65e9e4a2f3 | ||
![]() |
922b332766 | ||
![]() |
1ffa8fcbba | ||
![]() |
785786ecd1 | ||
![]() |
b78ad7c2b8 | ||
![]() |
4412ce3b86 | ||
![]() |
238430136e | ||
![]() |
15113ae854 | ||
![]() |
7ce6ae9421 | ||
![]() |
f9bc0c9dab | ||
![]() |
bf95658e24 | ||
![]() |
3c57475c8f | ||
![]() |
e8b16f0dfd | ||
![]() |
c1cf3679aa | ||
![]() |
871afd2bf2 | ||
![]() |
5595ef0783 | ||
![]() |
5a3e0c6b25 | ||
![]() |
a8e9ccbf1a | ||
![]() |
44708ed8bb | ||
![]() |
8e44d797a3 | ||
![]() |
419a92db43 | ||
![]() |
986853d497 | ||
![]() |
d4d233536f | ||
![]() |
21acdbbbfd | ||
![]() |
0146f35687 | ||
![]() |
2d5faaf3f8 | ||
![]() |
dd6d18102f | ||
![]() |
14d715e7fc | ||
![]() |
b820b7c47d | ||
![]() |
afe4647896 | ||
![]() |
4df186787a | ||
![]() |
b353f45d84 | ||
![]() |
fc2c195ed4 | ||
![]() |
f1d5f95f7c | ||
![]() |
59f935beb0 | ||
![]() |
ddb85cee7b | ||
![]() |
14bff5a375 | ||
![]() |
9311b02369 | ||
![]() |
82090f5060 | ||
![]() |
186a299215 | ||
![]() |
00068e88b0 | ||
![]() |
2c1a76cf92 | ||
![]() |
c65e72886c | ||
![]() |
e13f206a06 | ||
![]() |
8d405c4585 | ||
![]() |
2ac4d30736 | ||
![]() |
fa17e6d5ab | ||
![]() |
212b9df87d | ||
![]() |
44552937b6 | ||
![]() |
8f3c84b349 | ||
![]() |
29b8f76e57 | ||
![]() |
29838ce1ed | ||
![]() |
5cf753422b | ||
![]() |
ded5329f03 | ||
![]() |
e3fc59ff3d | ||
![]() |
6166a7191b | ||
![]() |
1169ac568b | ||
![]() |
1ab1503641 | ||
![]() |
2abd3844cf | ||
![]() |
854bdfb6f2 | ||
![]() |
5f8dcd45c1 | ||
![]() |
38bb8ef4d2 | ||
![]() |
7cc3102209 | ||
![]() |
525e220395 | ||
![]() |
140fd5adaf | ||
![]() |
5b2bf8fd17 | ||
![]() |
5e2de4531f | ||
![]() |
c33edbe5bb | ||
![]() |
85ba29012f | ||
![]() |
5975ec340b | ||
![]() |
3adfb86a19 | ||
![]() |
b3cbce3566 | ||
![]() |
7a2820ded9 | ||
![]() |
af4fb03230 | ||
![]() |
a5da21a426 | ||
![]() |
16e36dca97 | ||
![]() |
f522c6c8c7 | ||
![]() |
1bdbe90d2a | ||
![]() |
8ed1b1782e | ||
![]() |
31dd06bd12 | ||
![]() |
de7bbd3e24 | ||
![]() |
fbbc681ad4 | ||
![]() |
9e7f516d07 | ||
![]() |
066784c88f | ||
![]() |
81355a0e23 | ||
![]() |
215c7e0e14 | ||
![]() |
233284056a | ||
![]() |
56c69d9a25 | ||
![]() |
35cd6b9abf | ||
![]() |
823f27db5a | ||
![]() |
ad5101c5c0 | ||
![]() |
3bf389639b | ||
![]() |
31973de2d5 | ||
![]() |
524b48be7d | ||
![]() |
0c5ca3084e | ||
![]() |
49747684a0 | ||
![]() |
3495932eb0 | ||
![]() |
d73a4e1ed5 | ||
![]() |
14f5cab71d | ||
![]() |
29ab1935cb | ||
![]() |
bdc098645b | ||
![]() |
492874c4a0 | ||
![]() |
5e65d8d3c3 | ||
![]() |
1c329ff708 | ||
![]() |
e807274d7e | ||
![]() |
4950cbee1c | ||
![]() |
0bf64e9a2c | ||
![]() |
3076fc5f25 | ||
![]() |
b51d81edba | ||
![]() |
6faf9e8bbe | ||
![]() |
c0307dca3a | ||
![]() |
5b94807923 | ||
![]() |
4170eb0f83 | ||
![]() |
874c8fea03 | ||
![]() |
a4204b440c | ||
![]() |
f8e48a9230 | ||
![]() |
04231bcb54 | ||
![]() |
f170c80bea | ||
![]() |
394f16987d | ||
![]() |
f06c0a8b54 | ||
![]() |
36b157b85a | ||
![]() |
7a3c2e1f6c | ||
![]() |
fae80621fb | ||
![]() |
1edbdcb67b | ||
![]() |
99318b7b11 | ||
![]() |
a6107198b9 | ||
![]() |
203217c175 | ||
![]() |
c316735996 | ||
![]() |
40829d9d76 | ||
![]() |
68b077ffaa | ||
![]() |
8528b4db3a | ||
![]() |
48d1bc7c13 | ||
![]() |
08e85696c1 | ||
![]() |
e08ba6703c | ||
![]() |
9ef14efa29 | ||
![]() |
fc395de511 | ||
![]() |
751e2f4125 |
72
.coveragerc
72
.coveragerc
@@ -46,7 +46,6 @@ omit =
|
|||||||
homeassistant/components/android_ip_webcam/*
|
homeassistant/components/android_ip_webcam/*
|
||||||
homeassistant/components/anel_pwrctrl/switch.py
|
homeassistant/components/anel_pwrctrl/switch.py
|
||||||
homeassistant/components/anthemav/media_player.py
|
homeassistant/components/anthemav/media_player.py
|
||||||
homeassistant/components/apache_kafka/*
|
|
||||||
homeassistant/components/apcupsd/*
|
homeassistant/components/apcupsd/*
|
||||||
homeassistant/components/apple_tv/*
|
homeassistant/components/apple_tv/*
|
||||||
homeassistant/components/aqualogic/*
|
homeassistant/components/aqualogic/*
|
||||||
@@ -68,8 +67,8 @@ omit =
|
|||||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||||
homeassistant/components/avea/light.py
|
homeassistant/components/avea/light.py
|
||||||
homeassistant/components/avion/light.py
|
homeassistant/components/avion/light.py
|
||||||
|
homeassistant/components/avri/const.py
|
||||||
homeassistant/components/avri/sensor.py
|
homeassistant/components/avri/sensor.py
|
||||||
homeassistant/components/azure_event_hub/*
|
|
||||||
homeassistant/components/azure_service_bus/*
|
homeassistant/components/azure_service_bus/*
|
||||||
homeassistant/components/baidu/tts.py
|
homeassistant/components/baidu/tts.py
|
||||||
homeassistant/components/beewi_smartclim/sensor.py
|
homeassistant/components/beewi_smartclim/sensor.py
|
||||||
@@ -79,7 +78,12 @@ omit =
|
|||||||
homeassistant/components/bh1750/sensor.py
|
homeassistant/components/bh1750/sensor.py
|
||||||
homeassistant/components/bitcoin/sensor.py
|
homeassistant/components/bitcoin/sensor.py
|
||||||
homeassistant/components/bizkaibus/sensor.py
|
homeassistant/components/bizkaibus/sensor.py
|
||||||
homeassistant/components/blink/*
|
homeassistant/components/blink/__init__.py
|
||||||
|
homeassistant/components/blink/alarm_control_panel.py
|
||||||
|
homeassistant/components/blink/binary_sensor.py
|
||||||
|
homeassistant/components/blink/camera.py
|
||||||
|
homeassistant/components/blink/const.py
|
||||||
|
homeassistant/components/blink/sensor.py
|
||||||
homeassistant/components/blinksticklight/light.py
|
homeassistant/components/blinksticklight/light.py
|
||||||
homeassistant/components/blinkt/light.py
|
homeassistant/components/blinkt/light.py
|
||||||
homeassistant/components/blockchain/sensor.py
|
homeassistant/components/blockchain/sensor.py
|
||||||
@@ -154,9 +158,14 @@ omit =
|
|||||||
homeassistant/components/deluge/switch.py
|
homeassistant/components/deluge/switch.py
|
||||||
homeassistant/components/denon/media_player.py
|
homeassistant/components/denon/media_player.py
|
||||||
homeassistant/components/denonavr/media_player.py
|
homeassistant/components/denonavr/media_player.py
|
||||||
|
homeassistant/components/denonavr/receiver.py
|
||||||
homeassistant/components/deutsche_bahn/sensor.py
|
homeassistant/components/deutsche_bahn/sensor.py
|
||||||
homeassistant/components/devolo_home_control/__init__.py
|
homeassistant/components/devolo_home_control/__init__.py
|
||||||
|
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||||
homeassistant/components/devolo_home_control/const.py
|
homeassistant/components/devolo_home_control/const.py
|
||||||
|
homeassistant/components/devolo_home_control/devolo_device.py
|
||||||
|
homeassistant/components/devolo_home_control/sensor.py
|
||||||
|
homeassistant/components/devolo_home_control/subscriber.py
|
||||||
homeassistant/components/devolo_home_control/switch.py
|
homeassistant/components/devolo_home_control/switch.py
|
||||||
homeassistant/components/dht/sensor.py
|
homeassistant/components/dht/sensor.py
|
||||||
homeassistant/components/digital_ocean/*
|
homeassistant/components/digital_ocean/*
|
||||||
@@ -205,7 +214,14 @@ omit =
|
|||||||
homeassistant/components/emoncms_history/*
|
homeassistant/components/emoncms_history/*
|
||||||
homeassistant/components/emulated_hue/upnp.py
|
homeassistant/components/emulated_hue/upnp.py
|
||||||
homeassistant/components/enigma2/media_player.py
|
homeassistant/components/enigma2/media_player.py
|
||||||
homeassistant/components/enocean/*
|
homeassistant/components/enocean/__init__.py
|
||||||
|
homeassistant/components/enocean/binary_sensor.py
|
||||||
|
homeassistant/components/enocean/const.py
|
||||||
|
homeassistant/components/enocean/device.py
|
||||||
|
homeassistant/components/enocean/dongle.py
|
||||||
|
homeassistant/components/enocean/light.py
|
||||||
|
homeassistant/components/enocean/sensor.py
|
||||||
|
homeassistant/components/enocean/switch.py
|
||||||
homeassistant/components/enphase_envoy/sensor.py
|
homeassistant/components/enphase_envoy/sensor.py
|
||||||
homeassistant/components/entur_public_transport/*
|
homeassistant/components/entur_public_transport/*
|
||||||
homeassistant/components/environment_canada/*
|
homeassistant/components/environment_canada/*
|
||||||
@@ -255,7 +271,6 @@ omit =
|
|||||||
homeassistant/components/folder_watcher/*
|
homeassistant/components/folder_watcher/*
|
||||||
homeassistant/components/foobot/sensor.py
|
homeassistant/components/foobot/sensor.py
|
||||||
homeassistant/components/fortios/device_tracker.py
|
homeassistant/components/fortios/device_tracker.py
|
||||||
homeassistant/components/fortigate/*
|
|
||||||
homeassistant/components/foscam/camera.py
|
homeassistant/components/foscam/camera.py
|
||||||
homeassistant/components/foscam/const.py
|
homeassistant/components/foscam/const.py
|
||||||
homeassistant/components/foursquare/*
|
homeassistant/components/foursquare/*
|
||||||
@@ -284,6 +299,7 @@ omit =
|
|||||||
homeassistant/components/gitlab_ci/sensor.py
|
homeassistant/components/gitlab_ci/sensor.py
|
||||||
homeassistant/components/gitter/sensor.py
|
homeassistant/components/gitter/sensor.py
|
||||||
homeassistant/components/glances/__init__.py
|
homeassistant/components/glances/__init__.py
|
||||||
|
homeassistant/components/glances/const.py
|
||||||
homeassistant/components/glances/sensor.py
|
homeassistant/components/glances/sensor.py
|
||||||
homeassistant/components/gntp/notify.py
|
homeassistant/components/gntp/notify.py
|
||||||
homeassistant/components/goalfeed/*
|
homeassistant/components/goalfeed/*
|
||||||
@@ -304,6 +320,7 @@ omit =
|
|||||||
homeassistant/components/guardian/binary_sensor.py
|
homeassistant/components/guardian/binary_sensor.py
|
||||||
homeassistant/components/guardian/sensor.py
|
homeassistant/components/guardian/sensor.py
|
||||||
homeassistant/components/guardian/switch.py
|
homeassistant/components/guardian/switch.py
|
||||||
|
homeassistant/components/guardian/util.py
|
||||||
homeassistant/components/habitica/*
|
homeassistant/components/habitica/*
|
||||||
homeassistant/components/hangouts/*
|
homeassistant/components/hangouts/*
|
||||||
homeassistant/components/hangouts/__init__.py
|
homeassistant/components/hangouts/__init__.py
|
||||||
@@ -339,6 +356,8 @@ omit =
|
|||||||
homeassistant/components/hunterdouglas_powerview/sensor.py
|
homeassistant/components/hunterdouglas_powerview/sensor.py
|
||||||
homeassistant/components/hunterdouglas_powerview/cover.py
|
homeassistant/components/hunterdouglas_powerview/cover.py
|
||||||
homeassistant/components/hunterdouglas_powerview/entity.py
|
homeassistant/components/hunterdouglas_powerview/entity.py
|
||||||
|
homeassistant/components/hvv_departures/sensor.py
|
||||||
|
homeassistant/components/hvv_departures/__init__.py
|
||||||
homeassistant/components/hydrawise/*
|
homeassistant/components/hydrawise/*
|
||||||
homeassistant/components/hyperion/light.py
|
homeassistant/components/hyperion/light.py
|
||||||
homeassistant/components/ialarm/alarm_control_panel.py
|
homeassistant/components/ialarm/alarm_control_panel.py
|
||||||
@@ -361,7 +380,6 @@ omit =
|
|||||||
homeassistant/components/ihc/*
|
homeassistant/components/ihc/*
|
||||||
homeassistant/components/imap/sensor.py
|
homeassistant/components/imap/sensor.py
|
||||||
homeassistant/components/imap_email_content/sensor.py
|
homeassistant/components/imap_email_content/sensor.py
|
||||||
homeassistant/components/influxdb/sensor.py
|
|
||||||
homeassistant/components/insteon/*
|
homeassistant/components/insteon/*
|
||||||
homeassistant/components/incomfort/*
|
homeassistant/components/incomfort/*
|
||||||
homeassistant/components/intesishome/*
|
homeassistant/components/intesishome/*
|
||||||
@@ -431,7 +449,6 @@ omit =
|
|||||||
homeassistant/components/linux_battery/sensor.py
|
homeassistant/components/linux_battery/sensor.py
|
||||||
homeassistant/components/lirc/*
|
homeassistant/components/lirc/*
|
||||||
homeassistant/components/llamalab_automate/notify.py
|
homeassistant/components/llamalab_automate/notify.py
|
||||||
homeassistant/components/lockitron/lock.py
|
|
||||||
homeassistant/components/logi_circle/__init__.py
|
homeassistant/components/logi_circle/__init__.py
|
||||||
homeassistant/components/logi_circle/camera.py
|
homeassistant/components/logi_circle/camera.py
|
||||||
homeassistant/components/logi_circle/const.py
|
homeassistant/components/logi_circle/const.py
|
||||||
@@ -521,6 +538,7 @@ omit =
|
|||||||
homeassistant/components/netatmo/climate.py
|
homeassistant/components/netatmo/climate.py
|
||||||
homeassistant/components/netatmo/const.py
|
homeassistant/components/netatmo/const.py
|
||||||
homeassistant/components/netatmo/sensor.py
|
homeassistant/components/netatmo/sensor.py
|
||||||
|
homeassistant/components/netatmo/webhook.py
|
||||||
homeassistant/components/netdata/sensor.py
|
homeassistant/components/netdata/sensor.py
|
||||||
homeassistant/components/netgear/device_tracker.py
|
homeassistant/components/netgear/device_tracker.py
|
||||||
homeassistant/components/netgear_lte/*
|
homeassistant/components/netgear_lte/*
|
||||||
@@ -538,6 +556,7 @@ omit =
|
|||||||
homeassistant/components/notion/sensor.py
|
homeassistant/components/notion/sensor.py
|
||||||
homeassistant/components/noaa_tides/sensor.py
|
homeassistant/components/noaa_tides/sensor.py
|
||||||
homeassistant/components/norway_air/air_quality.py
|
homeassistant/components/norway_air/air_quality.py
|
||||||
|
homeassistant/components/notify_events/notify.py
|
||||||
homeassistant/components/nsw_fuel_station/sensor.py
|
homeassistant/components/nsw_fuel_station/sensor.py
|
||||||
homeassistant/components/nuimo_controller/*
|
homeassistant/components/nuimo_controller/*
|
||||||
homeassistant/components/nuki/lock.py
|
homeassistant/components/nuki/lock.py
|
||||||
@@ -610,9 +629,12 @@ omit =
|
|||||||
homeassistant/components/plugwise/climate.py
|
homeassistant/components/plugwise/climate.py
|
||||||
homeassistant/components/plugwise/sensor.py
|
homeassistant/components/plugwise/sensor.py
|
||||||
homeassistant/components/plugwise/switch.py
|
homeassistant/components/plugwise/switch.py
|
||||||
homeassistant/components/plum_lightpad/*
|
homeassistant/components/plum_lightpad/light.py
|
||||||
homeassistant/components/pocketcasts/sensor.py
|
homeassistant/components/pocketcasts/sensor.py
|
||||||
homeassistant/components/point/*
|
homeassistant/components/point/*
|
||||||
|
homeassistant/components/poolsense/__init__.py
|
||||||
|
homeassistant/components/poolsense/sensor.py
|
||||||
|
homeassistant/components/poolsense/binary_sensor.py
|
||||||
homeassistant/components/prezzibenzina/sensor.py
|
homeassistant/components/prezzibenzina/sensor.py
|
||||||
homeassistant/components/proliphix/climate.py
|
homeassistant/components/proliphix/climate.py
|
||||||
homeassistant/components/prometheus/*
|
homeassistant/components/prometheus/*
|
||||||
@@ -714,9 +736,15 @@ omit =
|
|||||||
homeassistant/components/sinch/*
|
homeassistant/components/sinch/*
|
||||||
homeassistant/components/slide/*
|
homeassistant/components/slide/*
|
||||||
homeassistant/components/sma/sensor.py
|
homeassistant/components/sma/sensor.py
|
||||||
homeassistant/components/smappee/*
|
homeassistant/components/smappee/__init__.py
|
||||||
|
homeassistant/components/smappee/api.py
|
||||||
|
homeassistant/components/smappee/binary_sensor.py
|
||||||
|
homeassistant/components/smappee/sensor.py
|
||||||
|
homeassistant/components/smappee/switch.py
|
||||||
homeassistant/components/smarty/*
|
homeassistant/components/smarty/*
|
||||||
homeassistant/components/smarthab/*
|
homeassistant/components/smarthab/__init__.py
|
||||||
|
homeassistant/components/smarthab/cover.py
|
||||||
|
homeassistant/components/smarthab/light.py
|
||||||
homeassistant/components/sms/*
|
homeassistant/components/sms/*
|
||||||
homeassistant/components/smtp/notify.py
|
homeassistant/components/smtp/notify.py
|
||||||
homeassistant/components/snapcast/*
|
homeassistant/components/snapcast/*
|
||||||
@@ -740,7 +768,8 @@ omit =
|
|||||||
homeassistant/components/spotcrime/sensor.py
|
homeassistant/components/spotcrime/sensor.py
|
||||||
homeassistant/components/spotify/__init__.py
|
homeassistant/components/spotify/__init__.py
|
||||||
homeassistant/components/spotify/media_player.py
|
homeassistant/components/spotify/media_player.py
|
||||||
homeassistant/components/squeezebox/*
|
homeassistant/components/squeezebox/__init__.py
|
||||||
|
homeassistant/components/squeezebox/media_player.py
|
||||||
homeassistant/components/starline/*
|
homeassistant/components/starline/*
|
||||||
homeassistant/components/starlingbank/sensor.py
|
homeassistant/components/starlingbank/sensor.py
|
||||||
homeassistant/components/steam_online/sensor.py
|
homeassistant/components/steam_online/sensor.py
|
||||||
@@ -797,6 +826,7 @@ omit =
|
|||||||
homeassistant/components/thomson/device_tracker.py
|
homeassistant/components/thomson/device_tracker.py
|
||||||
homeassistant/components/tibber/*
|
homeassistant/components/tibber/*
|
||||||
homeassistant/components/tikteck/light.py
|
homeassistant/components/tikteck/light.py
|
||||||
|
homeassistant/components/tile/__init__.py
|
||||||
homeassistant/components/tile/device_tracker.py
|
homeassistant/components/tile/device_tracker.py
|
||||||
homeassistant/components/time_date/sensor.py
|
homeassistant/components/time_date/sensor.py
|
||||||
homeassistant/components/tmb/sensor.py
|
homeassistant/components/tmb/sensor.py
|
||||||
@@ -804,7 +834,16 @@ omit =
|
|||||||
homeassistant/components/todoist/const.py
|
homeassistant/components/todoist/const.py
|
||||||
homeassistant/components/tof/sensor.py
|
homeassistant/components/tof/sensor.py
|
||||||
homeassistant/components/tomato/device_tracker.py
|
homeassistant/components/tomato/device_tracker.py
|
||||||
homeassistant/components/toon/*
|
homeassistant/components/toon/__init__.py
|
||||||
|
homeassistant/components/toon/binary_sensor.py
|
||||||
|
homeassistant/components/toon/climate.py
|
||||||
|
homeassistant/components/toon/const.py
|
||||||
|
homeassistant/components/toon/coordinator.py
|
||||||
|
homeassistant/components/toon/helpers.py
|
||||||
|
homeassistant/components/toon/models.py
|
||||||
|
homeassistant/components/toon/oauth2.py
|
||||||
|
homeassistant/components/toon/sensor.py
|
||||||
|
homeassistant/components/toon/switch.py
|
||||||
homeassistant/components/torque/sensor.py
|
homeassistant/components/torque/sensor.py
|
||||||
homeassistant/components/totalconnect/*
|
homeassistant/components/totalconnect/*
|
||||||
homeassistant/components/touchline/climate.py
|
homeassistant/components/touchline/climate.py
|
||||||
@@ -891,7 +930,14 @@ omit =
|
|||||||
homeassistant/components/xeoma/camera.py
|
homeassistant/components/xeoma/camera.py
|
||||||
homeassistant/components/xfinity/device_tracker.py
|
homeassistant/components/xfinity/device_tracker.py
|
||||||
homeassistant/components/xiaomi/camera.py
|
homeassistant/components/xiaomi/camera.py
|
||||||
homeassistant/components/xiaomi_aqara/*
|
homeassistant/components/xiaomi_aqara/__init__.py
|
||||||
|
homeassistant/components/xiaomi_aqara/binary_sensor.py
|
||||||
|
homeassistant/components/xiaomi_aqara/const.py
|
||||||
|
homeassistant/components/xiaomi_aqara/cover.py
|
||||||
|
homeassistant/components/xiaomi_aqara/light.py
|
||||||
|
homeassistant/components/xiaomi_aqara/lock.py
|
||||||
|
homeassistant/components/xiaomi_aqara/sensor.py
|
||||||
|
homeassistant/components/xiaomi_aqara/switch.py
|
||||||
homeassistant/components/xiaomi_miio/__init__.py
|
homeassistant/components/xiaomi_miio/__init__.py
|
||||||
homeassistant/components/xiaomi_miio/air_quality.py
|
homeassistant/components/xiaomi_miio/air_quality.py
|
||||||
homeassistant/components/xiaomi_miio/alarm_control_panel.py
|
homeassistant/components/xiaomi_miio/alarm_control_panel.py
|
||||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -8,3 +8,5 @@
|
|||||||
*.png binary
|
*.png binary
|
||||||
*.zip binary
|
*.zip binary
|
||||||
*.mp3 binary
|
*.mp3 binary
|
||||||
|
|
||||||
|
Dockerfile.dev linguist-language=Dockerfile
|
||||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
- Home Assistant Core release with the issue:
|
- Home Assistant Core release with the issue:
|
||||||
- Last working Home Assistant Core release (if known):
|
- Last working Home Assistant Core release (if known):
|
||||||
- Operating environment (Home Assistant/Supervised/Docker/venv):
|
- Operating environment (OS/Container/Supervised/Core):
|
||||||
- Integration causing this issue:
|
- Integration causing this issue:
|
||||||
- Link to integration documentation on our website:
|
- Link to integration documentation on our website:
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -25,7 +25,7 @@ about: Report an issue with Home Assistant Core
|
|||||||
|
|
||||||
- Home Assistant Core release with the issue:
|
- Home Assistant Core release with the issue:
|
||||||
- Last working Home Assistant Core release (if known):
|
- Last working Home Assistant Core release (if known):
|
||||||
- Operating environment (Home Assistant/Supervised/Docker/venv):
|
- Operating environment (OS/Container/Supervised/Core):
|
||||||
- Integration causing this issue:
|
- Integration causing this issue:
|
||||||
- Link to integration documentation on our website:
|
- Link to integration documentation on our website:
|
||||||
|
|
||||||
|
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "06:00"
|
||||||
|
open-pull-requests-limit: 10
|
784
.github/workflows/ci.yaml
vendored
Normal file
784
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,784 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- rc
|
||||||
|
- master
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: 3.7
|
||||||
|
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Separate job to pre-populate the base dependency cache
|
||||||
|
# This prevent upcoming jobs to do the same individually
|
||||||
|
prepare-base:
|
||||||
|
name: Prepare base dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-
|
||||||
|
- name: Create Python virtual environment
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -U pip setuptools
|
||||||
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
|
# Uninstalling typing as a workaround. Eventually we should make sure
|
||||||
|
# all our dependencies drop typing.
|
||||||
|
# Find offending deps with `pipdeptree -r -p typing`
|
||||||
|
pip uninstall -y typing
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pre-commit-
|
||||||
|
- name: Install pre-commit dependencies
|
||||||
|
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit install-hooks
|
||||||
|
|
||||||
|
lint-bandit:
|
||||||
|
name: Check bandit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run bandit
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
lint-black:
|
||||||
|
name: Check black
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run black
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual black --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
lint-codespell:
|
||||||
|
name: Check codespell
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register codespell problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||||
|
- name: Run codespell
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
|
||||||
|
|
||||||
|
lint-dockerfile:
|
||||||
|
name: Check Dockerfile
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Register hadolint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
|
- name: Check Dockerfile
|
||||||
|
uses: docker://hadolint/hadolint:v1.18.0
|
||||||
|
with:
|
||||||
|
args: hadolint Dockerfile
|
||||||
|
- name: Check Dockerfile.dev
|
||||||
|
uses: docker://hadolint/hadolint:v1.18.0
|
||||||
|
with:
|
||||||
|
args: hadolint Dockerfile.dev
|
||||||
|
|
||||||
|
lint-executable-shebangs:
|
||||||
|
name: Check executables
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register check executables problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||||
|
- name: Run executables check
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
|
||||||
|
|
||||||
|
lint-flake8:
|
||||||
|
name: Check flake8
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register flake8 problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/flake8.json"
|
||||||
|
- name: Run flake8
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual flake8 --all-files
|
||||||
|
|
||||||
|
lint-isort:
|
||||||
|
name: Check isort
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run isort
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
lint-json:
|
||||||
|
name: Check JSON
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register check-json problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/check-json.json"
|
||||||
|
- name: Run check-json
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual check-json --all-files
|
||||||
|
|
||||||
|
lint-pyupgrade:
|
||||||
|
name: Check pyupgrade
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run pyupgrade
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
# Disabled until we have the existing issues fixed
|
||||||
|
# lint-shellcheck:
|
||||||
|
# name: Check ShellCheck
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: prepare-base
|
||||||
|
# steps:
|
||||||
|
# - name: Check out code from GitHub
|
||||||
|
# uses: actions/checkout@v2
|
||||||
|
# - name: Run ShellCheck
|
||||||
|
# uses: ludeeus/action-shellcheck@0.3.0
|
||||||
|
|
||||||
|
lint-yaml:
|
||||||
|
name: Check YAML
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register yamllint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
|
||||||
|
- name: Run yamllint
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
hassfest:
|
||||||
|
name: Check hassfest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run hassfest
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
python -m script.hassfest --action validate
|
||||||
|
|
||||||
|
gen-requirements-all:
|
||||||
|
name: Check all requirements
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore base Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version
|
||||||
|
}}-${{ hashFiles('requirements.txt') }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run gen_requirements_all.py
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
python -m script.gen_requirements_all validate
|
||||||
|
|
||||||
|
prepare-tests:
|
||||||
|
name: Prepare tests for Python ${{ matrix.python-version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7, 3.8]
|
||||||
|
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name:
|
||||||
|
Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-
|
||||||
|
- name:
|
||||||
|
Create full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -U pip setuptools wheel
|
||||||
|
pip install -r requirements_all.txt
|
||||||
|
pip install -r requirements_test.txt
|
||||||
|
# Uninstalling typing as a workaround. Eventually we should make sure
|
||||||
|
# all our dependencies drop typing.
|
||||||
|
# Find offending deps with `pipdeptree -r -p typing`
|
||||||
|
pip uninstall -y typing
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
name: Check pylint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7]
|
||||||
|
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name:
|
||||||
|
Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register pylint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/pylint.json"
|
||||||
|
- name: Run pylint
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pylint homeassistant
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
name: Check mypy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7]
|
||||||
|
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name:
|
||||||
|
Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register mypy problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/mypy.json"
|
||||||
|
- name: Run mypy
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
mypy homeassistant
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare-tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
group: [1, 2, 3, 4]
|
||||||
|
python-version: [3.7, 3.8]
|
||||||
|
name: >-
|
||||||
|
Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }})
|
||||||
|
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name:
|
||||||
|
Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register Python problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/python.json"
|
||||||
|
- name: Install Pytest Annotation plugin
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
# Ideally this should be part of our dependencies
|
||||||
|
# However this plugin is fairly new and doesn't run correctly
|
||||||
|
# on a non-GitHub environment.
|
||||||
|
pip install pytest-github-actions-annotate-failures
|
||||||
|
- name: Run pytest
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pytest \
|
||||||
|
-qq \
|
||||||
|
--timeout=9 \
|
||||||
|
--durations=10 \
|
||||||
|
-n auto \
|
||||||
|
--dist=loadfile \
|
||||||
|
--test-group-count 4 \
|
||||||
|
--test-group=${{ matrix.group }} \
|
||||||
|
--cov homeassistant \
|
||||||
|
-o console_output_style=count \
|
||||||
|
-p no:sugar \
|
||||||
|
tests
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@2.1.0
|
||||||
|
with:
|
||||||
|
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
|
||||||
|
path: .coverage
|
||||||
|
- name: Check dirty
|
||||||
|
run: |
|
||||||
|
./script/check_dirty
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: Process test coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pytest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7]
|
||||||
|
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name:
|
||||||
|
Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-venv-${{ matrix.python-version }}-${{
|
||||||
|
hashFiles('requirements_test.txt') }}-${{
|
||||||
|
hashFiles('requirements_all.txt') }}-${{
|
||||||
|
hashFiles('homeassistant/package_constraints.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Download all coverage artifacts
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
- name: Combine coverage results
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
coverage combine coverage*/.coverage*
|
||||||
|
coverage report --fail-under=94
|
||||||
|
coverage xml
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v1.0.10
|
14
.github/workflows/matchers/check-executables-have-shebangs.json
vendored
Normal file
14
.github/workflows/matchers/check-executables-have-shebangs.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "check-executables-have-shebangs",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):\\s(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"message": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/check-json.json
vendored
Normal file
16
.github/workflows/matchers/check-json.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "check-json",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$",
|
||||||
|
"file": 1,
|
||||||
|
"message": 2,
|
||||||
|
"line": 3,
|
||||||
|
"column": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/codespell.json
vendored
Normal file
16
.github/workflows/matchers/codespell.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "codespell",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):\\s(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
30
.github/workflows/matchers/flake8.json
vendored
Normal file
30
.github/workflows/matchers/flake8.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "flake8-error",
|
||||||
|
"severity": "error",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "flake8-warning",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/hadolint.json
vendored
Normal file
16
.github/workflows/matchers/hadolint.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "hadolint",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"message": 3,
|
||||||
|
"code": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/mypy.json
vendored
Normal file
16
.github/workflows/matchers/mypy.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "mypy",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):\\s(error|warning):\\s(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"severity": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
32
.github/workflows/matchers/pylint.json
vendored
Normal file
32
.github/workflows/matchers/pylint.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "pylint-error",
|
||||||
|
"severity": "error",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4,
|
||||||
|
"code": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "pylint-warning",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4,
|
||||||
|
"code": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
18
.github/workflows/matchers/python.json
vendored
Normal file
18
.github/workflows/matchers/python.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "python",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
|
||||||
|
"message": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
.github/workflows/matchers/yamllint.json
vendored
Normal file
22
.github/workflows/matchers/yamllint.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "yamllint",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*\\.ya?ml)$",
|
||||||
|
"file": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$",
|
||||||
|
"line": 1,
|
||||||
|
"column": 2,
|
||||||
|
"severity": 3,
|
||||||
|
"message": 4,
|
||||||
|
"code": 5,
|
||||||
|
"loop": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -22,7 +22,7 @@ repos:
|
|||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json]
|
exclude_types: [csv, json]
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.8.1
|
rev: 3.8.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
25
.travis.yml
25
.travis.yml
@@ -1,4 +1,3 @@
|
|||||||
sudo: false
|
|
||||||
dist: bionic
|
dist: bionic
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
@@ -14,22 +13,30 @@ addons:
|
|||||||
sources:
|
sources:
|
||||||
- sourceline: ppa:savoury1/ffmpeg4
|
- sourceline: ppa:savoury1/ffmpeg4
|
||||||
|
|
||||||
matrix:
|
python:
|
||||||
|
- "3.7.1"
|
||||||
|
- "3.8"
|
||||||
|
|
||||||
|
env:
|
||||||
|
- TOX_ARGS="-- --test-group-count 4 --test-group 1"
|
||||||
|
- TOX_ARGS="-- --test-group-count 4 --test-group 2"
|
||||||
|
- TOX_ARGS="-- --test-group-count 4 --test-group 3"
|
||||||
|
- TOX_ARGS="-- --test-group-count 4 --test-group 4"
|
||||||
|
|
||||||
|
jobs:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- python: "3.7.0"
|
- python: "3.7.1"
|
||||||
env: TOXENV=lint
|
env: TOXENV=lint
|
||||||
- python: "3.7.0"
|
- python: "3.7.1"
|
||||||
env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30
|
env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30
|
||||||
- python: "3.7.0"
|
- python: "3.7.1"
|
||||||
env: TOXENV=typing
|
env: TOXENV=typing
|
||||||
- python: "3.7.0"
|
|
||||||
env: TOXENV=py37
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
pip: true
|
pip: true
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pre-commit
|
- $HOME/.cache/pre-commit
|
||||||
install: pip install -U tox
|
install: pip install -U tox tox-travis
|
||||||
language: python
|
language: python
|
||||||
script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop
|
script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop ${TOX_ARGS-}
|
||||||
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -76,7 +76,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Install all Requirements",
|
"label": "Install all Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt",
|
"command": "pip3 install -r requirements_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Install all Test Requirements",
|
"label": "Install all Test Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pip3 install -r requirements_test_all.txt -c homeassistant/package_constraints.txt",
|
"command": "pip3 install -r requirements_test_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
44
CODEOWNERS
44
CODEOWNERS
@@ -23,7 +23,6 @@ homeassistant/components/alarmdecoder/* @ajschmidt8
|
|||||||
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
||||||
homeassistant/components/almond/* @gcampax @balloob
|
homeassistant/components/almond/* @gcampax @balloob
|
||||||
homeassistant/components/alpha_vantage/* @fabaff
|
homeassistant/components/alpha_vantage/* @fabaff
|
||||||
homeassistant/components/amazon_polly/* @robbiet480
|
|
||||||
homeassistant/components/ambiclimate/* @danielhiversen
|
homeassistant/components/ambiclimate/* @danielhiversen
|
||||||
homeassistant/components/ambient_station/* @bachya
|
homeassistant/components/ambient_station/* @bachya
|
||||||
homeassistant/components/amcrest/* @pnbruckner
|
homeassistant/components/amcrest/* @pnbruckner
|
||||||
@@ -46,8 +45,8 @@ homeassistant/components/auth/* @home-assistant/core
|
|||||||
homeassistant/components/automation/* @home-assistant/core
|
homeassistant/components/automation/* @home-assistant/core
|
||||||
homeassistant/components/avea/* @pattyland
|
homeassistant/components/avea/* @pattyland
|
||||||
homeassistant/components/avri/* @timvancann
|
homeassistant/components/avri/* @timvancann
|
||||||
homeassistant/components/awair/* @danielsjf
|
homeassistant/components/awair/* @ahayworth @danielsjf
|
||||||
homeassistant/components/aws/* @awarecan @robbiet480
|
homeassistant/components/aws/* @awarecan
|
||||||
homeassistant/components/axis/* @Kane610
|
homeassistant/components/axis/* @Kane610
|
||||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||||
homeassistant/components/azure_service_bus/* @hfurubotten
|
homeassistant/components/azure_service_bus/* @hfurubotten
|
||||||
@@ -57,9 +56,10 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
|||||||
homeassistant/components/blebox/* @gadgetmobile
|
homeassistant/components/blebox/* @gadgetmobile
|
||||||
homeassistant/components/blink/* @fronzbot
|
homeassistant/components/blink/* @fronzbot
|
||||||
homeassistant/components/bmp280/* @belidzs
|
homeassistant/components/bmp280/* @belidzs
|
||||||
homeassistant/components/bmw_connected_drive/* @gerard33
|
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
|
||||||
homeassistant/components/bom/* @maddenp
|
homeassistant/components/bom/* @maddenp
|
||||||
homeassistant/components/braviatv/* @robbiet480 @bieniu
|
homeassistant/components/bond/* @prystupa
|
||||||
|
homeassistant/components/braviatv/* @bieniu
|
||||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||||
homeassistant/components/brother/* @bieniu
|
homeassistant/components/brother/* @bieniu
|
||||||
homeassistant/components/brunt/* @eavanvalkenburg
|
homeassistant/components/brunt/* @eavanvalkenburg
|
||||||
@@ -86,6 +86,7 @@ homeassistant/components/cpuspeed/* @fabaff
|
|||||||
homeassistant/components/cups/* @fabaff
|
homeassistant/components/cups/* @fabaff
|
||||||
homeassistant/components/daikin/* @fredrike
|
homeassistant/components/daikin/* @fredrike
|
||||||
homeassistant/components/darksky/* @fabaff
|
homeassistant/components/darksky/* @fabaff
|
||||||
|
homeassistant/components/debugpy/* @frenck
|
||||||
homeassistant/components/deconz/* @Kane610
|
homeassistant/components/deconz/* @Kane610
|
||||||
homeassistant/components/delijn/* @bollewolle @Emilv2
|
homeassistant/components/delijn/* @bollewolle @Emilv2
|
||||||
homeassistant/components/demo/* @home-assistant/core
|
homeassistant/components/demo/* @home-assistant/core
|
||||||
@@ -93,6 +94,7 @@ homeassistant/components/denonavr/* @scarface-4711 @starkillerOG
|
|||||||
homeassistant/components/derivative/* @afaucogney
|
homeassistant/components/derivative/* @afaucogney
|
||||||
homeassistant/components/device_automation/* @home-assistant/core
|
homeassistant/components/device_automation/* @home-assistant/core
|
||||||
homeassistant/components/devolo_home_control/* @2Fake @Shutgun
|
homeassistant/components/devolo_home_control/* @2Fake @Shutgun
|
||||||
|
homeassistant/components/dexcom/* @gagebenne
|
||||||
homeassistant/components/digital_ocean/* @fabaff
|
homeassistant/components/digital_ocean/* @fabaff
|
||||||
homeassistant/components/directv/* @ctalkington
|
homeassistant/components/directv/* @ctalkington
|
||||||
homeassistant/components/discogs/* @thibmaek
|
homeassistant/components/discogs/* @thibmaek
|
||||||
@@ -126,17 +128,14 @@ homeassistant/components/ezviz/* @baqs
|
|||||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||||
homeassistant/components/file/* @fabaff
|
homeassistant/components/file/* @fabaff
|
||||||
homeassistant/components/filter/* @dgomes
|
homeassistant/components/filter/* @dgomes
|
||||||
homeassistant/components/fitbit/* @robbiet480
|
|
||||||
homeassistant/components/fixer/* @fabaff
|
homeassistant/components/fixer/* @fabaff
|
||||||
homeassistant/components/flick_electric/* @ZephireNZ
|
homeassistant/components/flick_electric/* @ZephireNZ
|
||||||
homeassistant/components/flock/* @fabaff
|
homeassistant/components/flock/* @fabaff
|
||||||
homeassistant/components/flume/* @ChrisMandich @bdraco
|
homeassistant/components/flume/* @ChrisMandich @bdraco
|
||||||
homeassistant/components/flunearyou/* @bachya
|
homeassistant/components/flunearyou/* @bachya
|
||||||
homeassistant/components/forked_daapd/* @uvjustin
|
homeassistant/components/forked_daapd/* @uvjustin
|
||||||
homeassistant/components/fortigate/* @kifeo
|
|
||||||
homeassistant/components/fortios/* @kimfrellsen
|
homeassistant/components/fortios/* @kimfrellsen
|
||||||
homeassistant/components/foscam/* @skgsergio
|
homeassistant/components/foscam/* @skgsergio
|
||||||
homeassistant/components/foursquare/* @robbiet480
|
|
||||||
homeassistant/components/freebox/* @snoof85 @Quentame
|
homeassistant/components/freebox/* @snoof85 @Quentame
|
||||||
homeassistant/components/fronius/* @nielstron
|
homeassistant/components/fronius/* @nielstron
|
||||||
homeassistant/components/frontend/* @home-assistant/frontend
|
homeassistant/components/frontend/* @home-assistant/frontend
|
||||||
@@ -149,18 +148,15 @@ homeassistant/components/geonetnz_volcano/* @exxamalte
|
|||||||
homeassistant/components/gios/* @bieniu
|
homeassistant/components/gios/* @bieniu
|
||||||
homeassistant/components/gitter/* @fabaff
|
homeassistant/components/gitter/* @fabaff
|
||||||
homeassistant/components/glances/* @fabaff @engrbm87
|
homeassistant/components/glances/* @fabaff @engrbm87
|
||||||
homeassistant/components/gntp/* @robbiet480
|
|
||||||
homeassistant/components/gogogate2/* @vangorra
|
homeassistant/components/gogogate2/* @vangorra
|
||||||
homeassistant/components/google_assistant/* @home-assistant/cloud
|
homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||||
homeassistant/components/google_cloud/* @lufton
|
homeassistant/components/google_cloud/* @lufton
|
||||||
homeassistant/components/google_translate/* @awarecan
|
homeassistant/components/google_translate/* @awarecan
|
||||||
homeassistant/components/google_travel_time/* @robbiet480
|
|
||||||
homeassistant/components/gpsd/* @fabaff
|
homeassistant/components/gpsd/* @fabaff
|
||||||
homeassistant/components/greeneye_monitor/* @jkeljo
|
homeassistant/components/greeneye_monitor/* @jkeljo
|
||||||
homeassistant/components/griddy/* @bdraco
|
homeassistant/components/griddy/* @bdraco
|
||||||
homeassistant/components/group/* @home-assistant/core
|
homeassistant/components/group/* @home-assistant/core
|
||||||
homeassistant/components/growatt_server/* @indykoning
|
homeassistant/components/growatt_server/* @indykoning
|
||||||
homeassistant/components/gtfs/* @robbiet480
|
|
||||||
homeassistant/components/guardian/* @bachya
|
homeassistant/components/guardian/* @bachya
|
||||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
|
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
|
||||||
homeassistant/components/hassio/* @home-assistant/hass-io
|
homeassistant/components/hassio/* @home-assistant/hass-io
|
||||||
@@ -179,18 +175,20 @@ homeassistant/components/homekit_controller/* @Jc2k
|
|||||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||||
homeassistant/components/homematicip_cloud/* @SukramJ
|
homeassistant/components/homematicip_cloud/* @SukramJ
|
||||||
homeassistant/components/honeywell/* @zxdavb
|
homeassistant/components/honeywell/* @zxdavb
|
||||||
homeassistant/components/html5/* @robbiet480
|
|
||||||
homeassistant/components/http/* @home-assistant/core
|
homeassistant/components/http/* @home-assistant/core
|
||||||
homeassistant/components/huawei_lte/* @scop @fphammerle
|
homeassistant/components/huawei_lte/* @scop @fphammerle
|
||||||
homeassistant/components/huawei_router/* @abmantis
|
homeassistant/components/huawei_router/* @abmantis
|
||||||
homeassistant/components/hue/* @balloob
|
homeassistant/components/hue/* @balloob @frenck
|
||||||
|
homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
|
||||||
homeassistant/components/hunterdouglas_powerview/* @bdraco
|
homeassistant/components/hunterdouglas_powerview/* @bdraco
|
||||||
|
homeassistant/components/hvv_departures/* @vigonotion
|
||||||
|
homeassistant/components/hydrawise/* @ptcryan
|
||||||
homeassistant/components/iammeter/* @lewei50
|
homeassistant/components/iammeter/* @lewei50
|
||||||
homeassistant/components/iaqualink/* @flz
|
homeassistant/components/iaqualink/* @flz
|
||||||
homeassistant/components/icloud/* @Quentame
|
homeassistant/components/icloud/* @Quentame
|
||||||
homeassistant/components/ign_sismologia/* @exxamalte
|
homeassistant/components/ign_sismologia/* @exxamalte
|
||||||
homeassistant/components/incomfort/* @zxdavb
|
homeassistant/components/incomfort/* @zxdavb
|
||||||
homeassistant/components/influxdb/* @fabaff
|
homeassistant/components/influxdb/* @fabaff @mdegat01
|
||||||
homeassistant/components/input_boolean/* @home-assistant/core
|
homeassistant/components/input_boolean/* @home-assistant/core
|
||||||
homeassistant/components/input_datetime/* @home-assistant/core
|
homeassistant/components/input_datetime/* @home-assistant/core
|
||||||
homeassistant/components/input_number/* @home-assistant/core
|
homeassistant/components/input_number/* @home-assistant/core
|
||||||
@@ -243,6 +241,7 @@ homeassistant/components/melissa/* @kennedyshead
|
|||||||
homeassistant/components/met/* @danielhiversen
|
homeassistant/components/met/* @danielhiversen
|
||||||
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
|
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
|
||||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||||
|
homeassistant/components/metoffice/* @MrHarcombe
|
||||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||||
homeassistant/components/mikrotik/* @engrbm87
|
homeassistant/components/mikrotik/* @engrbm87
|
||||||
homeassistant/components/mill/* @danielhiversen
|
homeassistant/components/mill/* @danielhiversen
|
||||||
@@ -274,11 +273,12 @@ homeassistant/components/nissan_leaf/* @filcole
|
|||||||
homeassistant/components/nmbs/* @thibmaek
|
homeassistant/components/nmbs/* @thibmaek
|
||||||
homeassistant/components/no_ip/* @fabaff
|
homeassistant/components/no_ip/* @fabaff
|
||||||
homeassistant/components/notify/* @home-assistant/core
|
homeassistant/components/notify/* @home-assistant/core
|
||||||
|
homeassistant/components/notify_events/* @matrozov @papajojo
|
||||||
homeassistant/components/notion/* @bachya
|
homeassistant/components/notion/* @bachya
|
||||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||||
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
||||||
homeassistant/components/nuheat/* @bdraco
|
homeassistant/components/nuheat/* @bdraco
|
||||||
homeassistant/components/nuki/* @pvizeli
|
homeassistant/components/nuki/* @pschmitt @pvizeli
|
||||||
homeassistant/components/numato/* @clssn
|
homeassistant/components/numato/* @clssn
|
||||||
homeassistant/components/nut/* @bdraco
|
homeassistant/components/nut/* @bdraco
|
||||||
homeassistant/components/nws/* @MatthewFlamm
|
homeassistant/components/nws/* @MatthewFlamm
|
||||||
@@ -311,9 +311,11 @@ homeassistant/components/plaato/* @JohNan
|
|||||||
homeassistant/components/plant/* @ChristianKuehnel
|
homeassistant/components/plant/* @ChristianKuehnel
|
||||||
homeassistant/components/plex/* @jjlawren
|
homeassistant/components/plex/* @jjlawren
|
||||||
homeassistant/components/plugwise/* @CoMPaTech @bouwew
|
homeassistant/components/plugwise/* @CoMPaTech @bouwew
|
||||||
homeassistant/components/plum_lightpad/* @ColinHarrington
|
homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
|
||||||
homeassistant/components/point/* @fredrike
|
homeassistant/components/point/* @fredrike
|
||||||
|
homeassistant/components/poolsense/* @haemishkyd
|
||||||
homeassistant/components/powerwall/* @bdraco @jrester
|
homeassistant/components/powerwall/* @bdraco @jrester
|
||||||
|
homeassistant/components/prometheus/* @knyar
|
||||||
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
|
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
|
||||||
homeassistant/components/ps4/* @ktnrg45
|
homeassistant/components/ps4/* @ktnrg45
|
||||||
homeassistant/components/ptvsd/* @swamp-ig
|
homeassistant/components/ptvsd/* @swamp-ig
|
||||||
@@ -332,7 +334,7 @@ homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
|||||||
homeassistant/components/rainmachine/* @bachya
|
homeassistant/components/rainmachine/* @bachya
|
||||||
homeassistant/components/random/* @fabaff
|
homeassistant/components/random/* @fabaff
|
||||||
homeassistant/components/repetier/* @MTrab
|
homeassistant/components/repetier/* @MTrab
|
||||||
homeassistant/components/rfxtrx/* @danielhiversen
|
homeassistant/components/rfxtrx/* @danielhiversen @elupus
|
||||||
homeassistant/components/ring/* @balloob
|
homeassistant/components/ring/* @balloob
|
||||||
homeassistant/components/rmvtransport/* @cgtobi
|
homeassistant/components/rmvtransport/* @cgtobi
|
||||||
homeassistant/components/roku/* @ctalkington
|
homeassistant/components/roku/* @ctalkington
|
||||||
@@ -362,6 +364,7 @@ homeassistant/components/sinch/* @bendikrb
|
|||||||
homeassistant/components/sisyphus/* @jkeljo
|
homeassistant/components/sisyphus/* @jkeljo
|
||||||
homeassistant/components/slide/* @ualex73
|
homeassistant/components/slide/* @ualex73
|
||||||
homeassistant/components/sma/* @kellerza
|
homeassistant/components/sma/* @kellerza
|
||||||
|
homeassistant/components/smappee/* @bsmappee
|
||||||
homeassistant/components/smarthab/* @outadoc
|
homeassistant/components/smarthab/* @outadoc
|
||||||
homeassistant/components/smartthings/* @andrewsayre
|
homeassistant/components/smartthings/* @andrewsayre
|
||||||
homeassistant/components/smarty/* @z0mbieprocess
|
homeassistant/components/smarty/* @z0mbieprocess
|
||||||
@@ -375,7 +378,7 @@ homeassistant/components/somfy/* @tetienne
|
|||||||
homeassistant/components/sonarr/* @ctalkington
|
homeassistant/components/sonarr/* @ctalkington
|
||||||
homeassistant/components/songpal/* @rytilahti @shenxn
|
homeassistant/components/songpal/* @rytilahti @shenxn
|
||||||
homeassistant/components/spaceapi/* @fabaff
|
homeassistant/components/spaceapi/* @fabaff
|
||||||
homeassistant/components/speedtestdotnet/* @rohankapoorcom
|
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
|
||||||
homeassistant/components/spider/* @peternijssen
|
homeassistant/components/spider/* @peternijssen
|
||||||
homeassistant/components/spotify/* @frenck
|
homeassistant/components/spotify/* @frenck
|
||||||
homeassistant/components/sql/* @dgomes
|
homeassistant/components/sql/* @dgomes
|
||||||
@@ -424,8 +427,6 @@ homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
|||||||
homeassistant/components/tts/* @pvizeli
|
homeassistant/components/tts/* @pvizeli
|
||||||
homeassistant/components/tuya/* @ollo69
|
homeassistant/components/tuya/* @ollo69
|
||||||
homeassistant/components/twentemilieu/* @frenck
|
homeassistant/components/twentemilieu/* @frenck
|
||||||
homeassistant/components/twilio_call/* @robbiet480
|
|
||||||
homeassistant/components/twilio_sms/* @robbiet480
|
|
||||||
homeassistant/components/ubee/* @mzdrale
|
homeassistant/components/ubee/* @mzdrale
|
||||||
homeassistant/components/unifi/* @Kane610
|
homeassistant/components/unifi/* @Kane610
|
||||||
homeassistant/components/unifiled/* @florisvdk
|
homeassistant/components/unifiled/* @florisvdk
|
||||||
@@ -453,7 +454,6 @@ homeassistant/components/watson_tts/* @rutkai
|
|||||||
homeassistant/components/weather/* @fabaff
|
homeassistant/components/weather/* @fabaff
|
||||||
homeassistant/components/webostv/* @bendavid
|
homeassistant/components/webostv/* @bendavid
|
||||||
homeassistant/components/websocket_api/* @home-assistant/core
|
homeassistant/components/websocket_api/* @home-assistant/core
|
||||||
homeassistant/components/wemo/* @sqldiablo
|
|
||||||
homeassistant/components/wiffi/* @mampfes
|
homeassistant/components/wiffi/* @mampfes
|
||||||
homeassistant/components/withings/* @vangorra
|
homeassistant/components/withings/* @vangorra
|
||||||
homeassistant/components/wled/* @frenck
|
homeassistant/components/wled/* @frenck
|
||||||
@@ -472,7 +472,7 @@ homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
|||||||
homeassistant/components/yessssms/* @flowolf
|
homeassistant/components/yessssms/* @flowolf
|
||||||
homeassistant/components/yi/* @bachya
|
homeassistant/components/yi/* @bachya
|
||||||
homeassistant/components/yr/* @danielhiversen
|
homeassistant/components/yr/* @danielhiversen
|
||||||
homeassistant/components/zeroconf/* @robbiet480 @Kane610
|
homeassistant/components/zeroconf/* @Kane610
|
||||||
homeassistant/components/zerproc/* @emlove
|
homeassistant/components/zerproc/* @emlove
|
||||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||||
homeassistant/components/zone/* @home-assistant/core
|
homeassistant/components/zone/* @home-assistant/core
|
||||||
|
@@ -10,9 +10,10 @@ WORKDIR /usr/src
|
|||||||
COPY . homeassistant/
|
COPY . homeassistant/
|
||||||
RUN \
|
RUN \
|
||||||
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||||
-r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \
|
-r homeassistant/requirements_all.txt \
|
||||||
|
&& pip3 uninstall -y typing \
|
||||||
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||||
-e ./homeassistant \
|
-e ./homeassistant \
|
||||||
&& python3 -m compileall homeassistant/homeassistant
|
&& python3 -m compileall homeassistant/homeassistant
|
||||||
|
|
||||||
# Home Assistant S6-Overlay
|
# Home Assistant S6-Overlay
|
||||||
|
@@ -23,9 +23,10 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
|||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
COPY requirements_test.txt requirements_test_pre_commit.txt homeassistant/package_constraints.txt ./
|
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||||
RUN pip3 install -r requirements_test.txt -c package_constraints.txt \
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
&& rm -f requirements_test.txt package_constraints.txt requirements_test_pre_commit.txt
|
RUN pip3 install -r requirements_test.txt \
|
||||||
|
&& rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
# Set the default shell to bash instead of sh
|
||||||
ENV SHELL /bin/bash
|
ENV SHELL /bin/bash
|
||||||
|
@@ -44,7 +44,7 @@ stages:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
pip install -r requirements_test.txt
|
||||||
pre-commit install-hooks
|
pre-commit install-hooks
|
||||||
- script: |
|
- script: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -117,7 +117,7 @@ stages:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
pip install -r requirements_test.txt
|
||||||
pre-commit install-hooks
|
pre-commit install-hooks
|
||||||
- script: |
|
- script: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -165,7 +165,7 @@ stages:
|
|||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
|
pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt
|
||||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
pip install -r requirements_test_all.txt
|
||||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||||
# Find offending deps with `pipdeptree -r -p typing`
|
# Find offending deps with `pipdeptree -r -p typing`
|
||||||
pip uninstall -y typing
|
pip uninstall -y typing
|
||||||
@@ -209,8 +209,8 @@ stages:
|
|||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -U pip setuptools wheel
|
pip install -U pip setuptools wheel
|
||||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
pip install -r requirements_all.txt
|
||||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
pip install -r requirements_test.txt
|
||||||
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
|
||||||
# Find offending deps with `pipdeptree -r -p typing`
|
# Find offending deps with `pipdeptree -r -p typing`
|
||||||
pip uninstall -y typing
|
pip uninstall -y typing
|
||||||
@@ -234,7 +234,7 @@ stages:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
|
pip install -e . -r requirements_test.txt
|
||||||
pre-commit install-hooks
|
pre-commit install-hooks
|
||||||
- script: |
|
- script: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@@ -17,7 +17,7 @@ schedules:
|
|||||||
- dev
|
- dev
|
||||||
variables:
|
variables:
|
||||||
- name: versionWheels
|
- name: versionWheels
|
||||||
value: '1.10.1-3.7-alpine3.11'
|
value: '1.13.0-3.8-alpine3.12'
|
||||||
resources:
|
resources:
|
||||||
repositories:
|
repositories:
|
||||||
- repository: azure
|
- repository: azure
|
||||||
|
10
build.json
10
build.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-homeassistant",
|
"image": "homeassistant/{arch}-homeassistant",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-homeassistant-base:7.2.0",
|
"aarch64": "homeassistant/aarch64-homeassistant-base:8.0.0",
|
||||||
"armhf": "homeassistant/armhf-homeassistant-base:7.2.0",
|
"armhf": "homeassistant/armhf-homeassistant-base:8.0.0",
|
||||||
"armv7": "homeassistant/armv7-homeassistant-base:7.2.0",
|
"armv7": "homeassistant/armv7-homeassistant-base:8.0.0",
|
||||||
"amd64": "homeassistant/amd64-homeassistant-base:7.2.0",
|
"amd64": "homeassistant/amd64-homeassistant-base:8.0.0",
|
||||||
"i386": "homeassistant/i386-homeassistant-base:7.2.0"
|
"i386": "homeassistant/i386-homeassistant-base:8.0.0"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "core"
|
"io.hass.type": "core"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -8,32 +7,9 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import yarl
|
|
||||||
|
|
||||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||||
|
|
||||||
|
|
||||||
def set_loop() -> None:
|
|
||||||
"""Attempt to use different loop."""
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from asyncio.events import BaseDefaultEventLoopPolicy
|
|
||||||
|
|
||||||
if sys.platform == "win32":
|
|
||||||
if hasattr(asyncio, "WindowsProactorEventLoopPolicy"):
|
|
||||||
# pylint: disable=no-member
|
|
||||||
policy = asyncio.WindowsProactorEventLoopPolicy()
|
|
||||||
else:
|
|
||||||
|
|
||||||
class ProactorPolicy(BaseDefaultEventLoopPolicy):
|
|
||||||
"""Event loop policy to create proactor loops."""
|
|
||||||
|
|
||||||
_loop_factory = asyncio.ProactorEventLoop
|
|
||||||
|
|
||||||
policy = ProactorPolicy()
|
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(policy)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_python() -> None:
|
def validate_python() -> None:
|
||||||
"""Validate that the right Python version is running."""
|
"""Validate that the right Python version is running."""
|
||||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||||
@@ -240,39 +216,6 @@ def cmdline() -> List[str]:
|
|||||||
return [arg for arg in sys.argv if arg != "--daemon"]
|
return [arg for arg in sys.argv if arg != "--daemon"]
|
||||||
|
|
||||||
|
|
||||||
async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
|
|
||||||
"""Set up Home Assistant and run."""
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from homeassistant import bootstrap
|
|
||||||
|
|
||||||
hass = await bootstrap.async_setup_hass(
|
|
||||||
config_dir=config_dir,
|
|
||||||
verbose=args.verbose,
|
|
||||||
log_rotate_days=args.log_rotate_days,
|
|
||||||
log_file=args.log_file,
|
|
||||||
log_no_color=args.log_no_color,
|
|
||||||
skip_pip=args.skip_pip,
|
|
||||||
safe_mode=args.safe_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
if hass is None:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if args.open_ui:
|
|
||||||
import webbrowser # pylint: disable=import-outside-toplevel
|
|
||||||
|
|
||||||
if hass.config.api is not None:
|
|
||||||
scheme = "https" if hass.config.api.use_ssl else "http"
|
|
||||||
url = str(
|
|
||||||
yarl.URL.build(
|
|
||||||
scheme=scheme, host="127.0.0.1", port=hass.config.api.port
|
|
||||||
)
|
|
||||||
)
|
|
||||||
hass.add_job(webbrowser.open, url)
|
|
||||||
|
|
||||||
return await hass.async_run()
|
|
||||||
|
|
||||||
|
|
||||||
def try_to_restart() -> None:
|
def try_to_restart() -> None:
|
||||||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||||
# Things should be mostly shut down already at this point, now just try
|
# Things should be mostly shut down already at this point, now just try
|
||||||
@@ -319,8 +262,6 @@ def main() -> int:
|
|||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
validate_python()
|
validate_python()
|
||||||
|
|
||||||
set_loop()
|
|
||||||
|
|
||||||
# Run a simple daemon runner process on Windows to handle restarts
|
# Run a simple daemon runner process on Windows to handle restarts
|
||||||
if os.name == "nt" and "--runner" not in sys.argv:
|
if os.name == "nt" and "--runner" not in sys.argv:
|
||||||
nt_args = cmdline() + ["--runner"]
|
nt_args = cmdline() + ["--runner"]
|
||||||
@@ -353,7 +294,22 @@ def main() -> int:
|
|||||||
if args.pid_file:
|
if args.pid_file:
|
||||||
write_pid(args.pid_file)
|
write_pid(args.pid_file)
|
||||||
|
|
||||||
exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug)
|
# pylint: disable=import-outside-toplevel
|
||||||
|
from homeassistant import runner
|
||||||
|
|
||||||
|
runtime_conf = runner.RuntimeConfig(
|
||||||
|
config_dir=config_dir,
|
||||||
|
verbose=args.verbose,
|
||||||
|
log_rotate_days=args.log_rotate_days,
|
||||||
|
log_file=args.log_file,
|
||||||
|
log_no_color=args.log_no_color,
|
||||||
|
skip_pip=args.skip_pip,
|
||||||
|
safe_mode=args.safe_mode,
|
||||||
|
debug=args.debug,
|
||||||
|
open_ui=args.open_ui,
|
||||||
|
)
|
||||||
|
|
||||||
|
exit_code = runner.run(runtime_conf)
|
||||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||||
try_to_restart()
|
try_to_restart()
|
||||||
|
|
||||||
|
@@ -77,10 +77,10 @@ def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
|||||||
class NotifySetting:
|
class NotifySetting:
|
||||||
"""Store notify setting for one user."""
|
"""Store notify setting for one user."""
|
||||||
|
|
||||||
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
|
secret: str = attr.ib(factory=_generate_secret) # not persistent
|
||||||
counter = attr.ib(type=int, factory=_generate_random) # not persistent
|
counter: int = attr.ib(factory=_generate_random) # not persistent
|
||||||
notify_service = attr.ib(type=Optional[str], default=None)
|
notify_service: Optional[str] = attr.ib(default=None)
|
||||||
target = attr.ib(type=Optional[str], default=None)
|
target: Optional[str] = attr.ib(default=None)
|
||||||
|
|
||||||
|
|
||||||
_UsersDict = Dict[str, NotifySetting]
|
_UsersDict = Dict[str, NotifySetting]
|
||||||
|
@@ -117,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
"""
|
"""
|
||||||
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
user = await self.hass.auth.async_get_user(user_id)
|
||||||
|
assert user is not None
|
||||||
return TotpSetupFlow(self, self.input_schema, user)
|
return TotpSetupFlow(self, self.input_schema, user)
|
||||||
|
|
||||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||||
|
@@ -20,39 +20,35 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
|||||||
class Group:
|
class Group:
|
||||||
"""A group."""
|
"""A group."""
|
||||||
|
|
||||||
name = attr.ib(type=Optional[str])
|
name: Optional[str] = attr.ib()
|
||||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
policy: perm_mdl.PolicyType = attr.ib()
|
||||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||||
system_generated = attr.ib(type=bool, default=False)
|
system_generated: bool = attr.ib(default=False)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class User:
|
class User:
|
||||||
"""A user."""
|
"""A user."""
|
||||||
|
|
||||||
name = attr.ib(type=Optional[str])
|
name: Optional[str] = attr.ib()
|
||||||
perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False)
|
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
|
||||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||||
is_owner = attr.ib(type=bool, default=False)
|
is_owner: bool = attr.ib(default=False)
|
||||||
is_active = attr.ib(type=bool, default=False)
|
is_active: bool = attr.ib(default=False)
|
||||||
system_generated = attr.ib(type=bool, default=False)
|
system_generated: bool = attr.ib(default=False)
|
||||||
|
|
||||||
groups = attr.ib(type=List[Group], factory=list, eq=False, order=False)
|
groups: List[Group] = attr.ib(factory=list, eq=False, order=False)
|
||||||
|
|
||||||
# List of credentials of a user.
|
# List of credentials of a user.
|
||||||
credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False)
|
credentials: List["Credentials"] = attr.ib(factory=list, eq=False, order=False)
|
||||||
|
|
||||||
# Tokens associated with a user.
|
# Tokens associated with a user.
|
||||||
refresh_tokens = attr.ib(
|
refresh_tokens: Dict[str, "RefreshToken"] = attr.ib(
|
||||||
type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False
|
factory=dict, eq=False, order=False
|
||||||
)
|
)
|
||||||
|
|
||||||
_permissions = attr.ib(
|
_permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib(
|
||||||
type=Optional[perm_mdl.PolicyPermissions],
|
init=False, eq=False, order=False, default=None,
|
||||||
init=False,
|
|
||||||
eq=False,
|
|
||||||
order=False,
|
|
||||||
default=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -88,39 +84,38 @@ class User:
|
|||||||
class RefreshToken:
|
class RefreshToken:
|
||||||
"""RefreshToken for a user to grant new access tokens."""
|
"""RefreshToken for a user to grant new access tokens."""
|
||||||
|
|
||||||
user = attr.ib(type=User)
|
user: User = attr.ib()
|
||||||
client_id = attr.ib(type=Optional[str])
|
client_id: Optional[str] = attr.ib()
|
||||||
access_token_expiration = attr.ib(type=timedelta)
|
access_token_expiration: timedelta = attr.ib()
|
||||||
client_name = attr.ib(type=Optional[str], default=None)
|
client_name: Optional[str] = attr.ib(default=None)
|
||||||
client_icon = attr.ib(type=Optional[str], default=None)
|
client_icon: Optional[str] = attr.ib(default=None)
|
||||||
token_type = attr.ib(
|
token_type: str = attr.ib(
|
||||||
type=str,
|
|
||||||
default=TOKEN_TYPE_NORMAL,
|
default=TOKEN_TYPE_NORMAL,
|
||||||
validator=attr.validators.in_(
|
validator=attr.validators.in_(
|
||||||
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
|
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||||
created_at = attr.ib(type=datetime, factory=dt_util.utcnow)
|
created_at: datetime = attr.ib(factory=dt_util.utcnow)
|
||||||
token = attr.ib(type=str, factory=lambda: secrets.token_hex(64))
|
token: str = attr.ib(factory=lambda: secrets.token_hex(64))
|
||||||
jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64))
|
jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64))
|
||||||
|
|
||||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
last_used_at: Optional[datetime] = attr.ib(default=None)
|
||||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
last_used_ip: Optional[str] = attr.ib(default=None)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class Credentials:
|
class Credentials:
|
||||||
"""Credentials for a user on an auth provider."""
|
"""Credentials for a user on an auth provider."""
|
||||||
|
|
||||||
auth_provider_type = attr.ib(type=str)
|
auth_provider_type: str = attr.ib()
|
||||||
auth_provider_id = attr.ib(type=Optional[str])
|
auth_provider_id: Optional[str] = attr.ib()
|
||||||
|
|
||||||
# Allow the auth provider to store data to represent their auth.
|
# Allow the auth provider to store data to represent their auth.
|
||||||
data = attr.ib(type=dict)
|
data: dict = attr.ib()
|
||||||
|
|
||||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||||
is_new = attr.ib(type=bool, default=True)
|
is_new: bool = attr.ib(default=True)
|
||||||
|
|
||||||
|
|
||||||
class UserMeta(NamedTuple):
|
class UserMeta(NamedTuple):
|
||||||
|
@@ -13,5 +13,5 @@ if TYPE_CHECKING:
|
|||||||
class PermissionLookup:
|
class PermissionLookup:
|
||||||
"""Class to hold data for permission lookups."""
|
"""Class to hold data for permission lookups."""
|
||||||
|
|
||||||
entity_registry = attr.ib(type="ent_reg.EntityRegistry")
|
entity_registry: "ent_reg.EntityRegistry" = attr.ib()
|
||||||
device_registry = attr.ib(type="dev_reg.DeviceRegistry")
|
device_registry: "dev_reg.DeviceRegistry" = attr.ib()
|
||||||
|
@@ -175,7 +175,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
"""Initialize the login flow."""
|
"""Initialize the login flow."""
|
||||||
self._auth_provider = auth_provider
|
self._auth_provider = auth_provider
|
||||||
self._auth_module_id: Optional[str] = None
|
self._auth_module_id: Optional[str] = None
|
||||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
self._auth_manager = auth_provider.hass.auth
|
||||||
self.available_mfa_modules: Dict[str, str] = {}
|
self.available_mfa_modules: Dict[str, str] = {}
|
||||||
self.created_at = dt_util.utcnow()
|
self.created_at = dt_util.utcnow()
|
||||||
self.invalid_mfa_times = 0
|
self.invalid_mfa_times = 0
|
||||||
@@ -224,6 +224,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
|
assert self._auth_module_id is not None
|
||||||
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
|
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
|
||||||
if auth_module is None:
|
if auth_module is None:
|
||||||
# Given an invalid input to async_step_select_mfa_module
|
# Given an invalid input to async_step_select_mfa_module
|
||||||
@@ -234,7 +235,9 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
auth_module, "async_initialize_login_mfa_step"
|
auth_module, "async_initialize_login_mfa_step"
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
await auth_module.async_initialize_login_mfa_step( # type: ignore
|
||||||
|
self.user.id
|
||||||
|
)
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
_LOGGER.exception("Error initializing MFA step")
|
_LOGGER.exception("Error initializing MFA step")
|
||||||
return self.async_abort(reason="unknown_error")
|
return self.async_abort(reason="unknown_error")
|
||||||
|
@@ -75,7 +75,7 @@ class CommandLineAuthProvider(AuthProvider):
|
|||||||
|
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"User %r failed to authenticate, command exited with code %d.",
|
"User %r failed to authenticate, command exited with code %d",
|
||||||
username,
|
username,
|
||||||
process.returncode,
|
process.returncode,
|
||||||
)
|
)
|
||||||
|
@@ -190,7 +190,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
|||||||
).async_validate_access(self._ip_address)
|
).async_validate_access(self._ip_address)
|
||||||
|
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
return self.async_abort(reason="not_whitelisted")
|
return self.async_abort(reason="not_allowed")
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return await self.async_finish(user_input)
|
return await self.async_finish(user_input)
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import Any, Dict, Optional, Set
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||||
|
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
import yarl
|
||||||
|
|
||||||
from homeassistant import config as conf_util, config_entries, core, loader
|
from homeassistant import config as conf_util, config_entries, core, loader
|
||||||
from homeassistant.components import http
|
from homeassistant.components import http
|
||||||
@@ -20,11 +22,19 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.setup import DATA_SETUP, DATA_SETUP_STARTED, async_setup_component
|
from homeassistant.setup import (
|
||||||
|
DATA_SETUP,
|
||||||
|
DATA_SETUP_STARTED,
|
||||||
|
async_set_domains_to_be_loaded,
|
||||||
|
async_setup_component,
|
||||||
|
)
|
||||||
from homeassistant.util.logging import async_activate_log_queue_handler
|
from homeassistant.util.logging import async_activate_log_queue_handler
|
||||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||||
from homeassistant.util.yaml import clear_secret_cache
|
from homeassistant.util.yaml import clear_secret_cache
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .runner import RuntimeConfig
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||||
@@ -34,12 +44,18 @@ DATA_LOGGING = "logging"
|
|||||||
|
|
||||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||||
|
|
||||||
DEBUGGER_INTEGRATIONS = {"ptvsd"}
|
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
||||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||||
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"}
|
LOGGING_INTEGRATIONS = {
|
||||||
STAGE_1_INTEGRATIONS = {
|
# Set log levels
|
||||||
|
"logger",
|
||||||
|
# Error logging
|
||||||
|
"system_log",
|
||||||
|
"sentry",
|
||||||
# To record data
|
# To record data
|
||||||
"recorder",
|
"recorder",
|
||||||
|
}
|
||||||
|
STAGE_1_INTEGRATIONS = {
|
||||||
# To make sure we forward data to other instances
|
# To make sure we forward data to other instances
|
||||||
"mqtt_eventstream",
|
"mqtt_eventstream",
|
||||||
# To provide account link implementations
|
# To provide account link implementations
|
||||||
@@ -50,28 +66,26 @@ STAGE_1_INTEGRATIONS = {
|
|||||||
# as possible so problem integrations can
|
# as possible so problem integrations can
|
||||||
# be removed
|
# be removed
|
||||||
"frontend",
|
"frontend",
|
||||||
"config",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_hass(
|
async def async_setup_hass(
|
||||||
*,
|
runtime_config: "RuntimeConfig",
|
||||||
config_dir: str,
|
|
||||||
verbose: bool,
|
|
||||||
log_rotate_days: int,
|
|
||||||
log_file: str,
|
|
||||||
log_no_color: bool,
|
|
||||||
skip_pip: bool,
|
|
||||||
safe_mode: bool,
|
|
||||||
) -> Optional[core.HomeAssistant]:
|
) -> Optional[core.HomeAssistant]:
|
||||||
"""Set up Home Assistant."""
|
"""Set up Home Assistant."""
|
||||||
hass = core.HomeAssistant()
|
hass = core.HomeAssistant()
|
||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = runtime_config.config_dir
|
||||||
|
|
||||||
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
async_enable_logging(
|
||||||
|
hass,
|
||||||
|
runtime_config.verbose,
|
||||||
|
runtime_config.log_rotate_days,
|
||||||
|
runtime_config.log_file,
|
||||||
|
runtime_config.log_no_color,
|
||||||
|
)
|
||||||
|
|
||||||
hass.config.skip_pip = skip_pip
|
hass.config.skip_pip = runtime_config.skip_pip
|
||||||
if skip_pip:
|
if runtime_config.skip_pip:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Skipping pip installation of required modules. This may cause issues"
|
"Skipping pip installation of required modules. This may cause issues"
|
||||||
)
|
)
|
||||||
@@ -80,10 +94,11 @@ async def async_setup_hass(
|
|||||||
_LOGGER.error("Error getting configuration path")
|
_LOGGER.error("Error getting configuration path")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_LOGGER.info("Config directory: %s", config_dir)
|
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
||||||
|
|
||||||
config_dict = None
|
config_dict = None
|
||||||
basic_setup_success = False
|
basic_setup_success = False
|
||||||
|
safe_mode = runtime_config.safe_mode
|
||||||
|
|
||||||
if not safe_mode:
|
if not safe_mode:
|
||||||
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||||
@@ -96,7 +111,7 @@ async def async_setup_hass(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not is_virtual_env():
|
if not is_virtual_env():
|
||||||
await async_mount_local_lib_path(config_dir)
|
await async_mount_local_lib_path(runtime_config.config_dir)
|
||||||
|
|
||||||
basic_setup_success = (
|
basic_setup_success = (
|
||||||
await async_from_config_dict(config_dict, hass) is not None
|
await async_from_config_dict(config_dict, hass) is not None
|
||||||
@@ -125,8 +140,13 @@ async def async_setup_hass(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
safe_mode = True
|
safe_mode = True
|
||||||
|
old_config = hass.config
|
||||||
|
|
||||||
hass = core.HomeAssistant()
|
hass = core.HomeAssistant()
|
||||||
hass.config.config_dir = config_dir
|
hass.config.skip_pip = old_config.skip_pip
|
||||||
|
hass.config.internal_url = old_config.internal_url
|
||||||
|
hass.config.external_url = old_config.external_url
|
||||||
|
hass.config.config_dir = old_config.config_dir
|
||||||
|
|
||||||
if safe_mode:
|
if safe_mode:
|
||||||
_LOGGER.info("Starting in safe mode")
|
_LOGGER.info("Starting in safe mode")
|
||||||
@@ -138,9 +158,32 @@ async def async_setup_hass(
|
|||||||
{"safe_mode": {}, "http": http_conf}, hass,
|
{"safe_mode": {}, "http": http_conf}, hass,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if runtime_config.open_ui:
|
||||||
|
hass.add_job(open_hass_ui, hass)
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||||
|
"""Open the UI."""
|
||||||
|
import webbrowser # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
|
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||||
|
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||||
|
return
|
||||||
|
|
||||||
|
scheme = "https" if hass.config.api.use_ssl else "http"
|
||||||
|
url = str(
|
||||||
|
yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not webbrowser.open(url):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to open the Home Assistant UI in a browser. Open it yourself at %s",
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_from_config_dict(
|
async def async_from_config_dict(
|
||||||
config: ConfigType, hass: core.HomeAssistant
|
config: ConfigType, hass: core.HomeAssistant
|
||||||
) -> Optional[core.HomeAssistant]:
|
) -> Optional[core.HomeAssistant]:
|
||||||
@@ -327,76 +370,130 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
|||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_log_pending_setups(
|
||||||
|
domains: Set[str], setup_started: Dict[str, datetime]
|
||||||
|
) -> None:
|
||||||
|
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
|
||||||
|
remaining = [domain for domain in domains if domain in setup_started]
|
||||||
|
|
||||||
|
if remaining:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Waiting on integrations to complete setup: %s", ", ".join(remaining),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_multi_components(
|
||||||
|
hass: core.HomeAssistant,
|
||||||
|
domains: Set[str],
|
||||||
|
config: Dict[str, Any],
|
||||||
|
setup_started: Dict[str, datetime],
|
||||||
|
) -> None:
|
||||||
|
"""Set up multiple domains. Log on failure."""
|
||||||
|
futures = {
|
||||||
|
domain: hass.async_create_task(async_setup_component(hass, domain, config))
|
||||||
|
for domain in domains
|
||||||
|
}
|
||||||
|
log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started))
|
||||||
|
await asyncio.wait(futures.values())
|
||||||
|
log_task.cancel()
|
||||||
|
errors = [domain for domain in domains if futures[domain].exception()]
|
||||||
|
for domain in errors:
|
||||||
|
exception = futures[domain].exception()
|
||||||
|
assert exception is not None
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error setting up integration %s - received exception",
|
||||||
|
domain,
|
||||||
|
exc_info=(type(exception), exception, exception.__traceback__),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _async_set_up_integrations(
|
async def _async_set_up_integrations(
|
||||||
hass: core.HomeAssistant, config: Dict[str, Any]
|
hass: core.HomeAssistant, config: Dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up all the integrations."""
|
"""Set up all the integrations."""
|
||||||
|
|
||||||
setup_started = hass.data[DATA_SETUP_STARTED] = {}
|
setup_started = hass.data[DATA_SETUP_STARTED] = {}
|
||||||
|
domains_to_setup = _get_domains(hass, config)
|
||||||
|
|
||||||
async def async_setup_multi_components(domains: Set[str]) -> None:
|
# Resolve all dependencies so we know all integrations
|
||||||
"""Set up multiple domains. Log on failure."""
|
# that will have to be loaded and start rightaway
|
||||||
|
integration_cache: Dict[str, loader.Integration] = {}
|
||||||
|
to_resolve = domains_to_setup
|
||||||
|
while to_resolve:
|
||||||
|
old_to_resolve = to_resolve
|
||||||
|
to_resolve = set()
|
||||||
|
|
||||||
async def _async_log_pending_setups() -> None:
|
integrations_to_process = [
|
||||||
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
|
int_or_exc
|
||||||
while True:
|
for int_or_exc in await asyncio.gather(
|
||||||
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
|
*(
|
||||||
remaining = [domain for domain in domains if domain in setup_started]
|
loader.async_get_integration(hass, domain)
|
||||||
|
for domain in old_to_resolve
|
||||||
if remaining:
|
),
|
||||||
_LOGGER.info(
|
return_exceptions=True,
|
||||||
"Waiting on integrations to complete setup: %s",
|
|
||||||
", ".join(remaining),
|
|
||||||
)
|
|
||||||
|
|
||||||
futures = {
|
|
||||||
domain: hass.async_create_task(async_setup_component(hass, domain, config))
|
|
||||||
for domain in domains
|
|
||||||
}
|
|
||||||
log_task = asyncio.create_task(_async_log_pending_setups())
|
|
||||||
await asyncio.wait(futures.values())
|
|
||||||
log_task.cancel()
|
|
||||||
errors = [domain for domain in domains if futures[domain].exception()]
|
|
||||||
for domain in errors:
|
|
||||||
exception = futures[domain].exception()
|
|
||||||
_LOGGER.error(
|
|
||||||
"Error setting up integration %s - received exception",
|
|
||||||
domain,
|
|
||||||
exc_info=(type(exception), exception, exception.__traceback__),
|
|
||||||
)
|
)
|
||||||
|
if isinstance(int_or_exc, loader.Integration)
|
||||||
|
]
|
||||||
|
resolve_dependencies_tasks = [
|
||||||
|
itg.resolve_dependencies()
|
||||||
|
for itg in integrations_to_process
|
||||||
|
if not itg.all_dependencies_resolved
|
||||||
|
]
|
||||||
|
|
||||||
domains = _get_domains(hass, config)
|
if resolve_dependencies_tasks:
|
||||||
|
await asyncio.gather(*resolve_dependencies_tasks)
|
||||||
|
|
||||||
|
for itg in integrations_to_process:
|
||||||
|
integration_cache[itg.domain] = itg
|
||||||
|
|
||||||
|
for dep in itg.all_dependencies:
|
||||||
|
if dep in domains_to_setup:
|
||||||
|
continue
|
||||||
|
|
||||||
|
domains_to_setup.add(dep)
|
||||||
|
to_resolve.add(dep)
|
||||||
|
|
||||||
|
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
|
||||||
|
|
||||||
|
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
|
||||||
|
|
||||||
|
# Load logging as soon as possible
|
||||||
|
if logging_domains:
|
||||||
|
_LOGGER.info("Setting up logging: %s", logging_domains)
|
||||||
|
await async_setup_multi_components(hass, logging_domains, config, setup_started)
|
||||||
|
|
||||||
# Start up debuggers. Start these first in case they want to wait.
|
# Start up debuggers. Start these first in case they want to wait.
|
||||||
debuggers = domains & DEBUGGER_INTEGRATIONS
|
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
|
||||||
|
|
||||||
if debuggers:
|
if debuggers:
|
||||||
_LOGGER.debug("Starting up debuggers %s", debuggers)
|
_LOGGER.debug("Setting up debuggers: %s", debuggers)
|
||||||
await async_setup_multi_components(debuggers)
|
await async_setup_multi_components(hass, debuggers, config, setup_started)
|
||||||
domains -= DEBUGGER_INTEGRATIONS
|
|
||||||
|
|
||||||
# Resolve all dependencies of all components so we can find the logging
|
# calculate what components to setup in what stage
|
||||||
# and integrations that need faster initialization.
|
stage_1_domains = set()
|
||||||
resolved_domains_task = asyncio.gather(
|
|
||||||
*(loader.async_component_dependencies(hass, domain) for domain in domains),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Finish resolving domains
|
# Find all dependencies of any dependency of any stage 1 integration that
|
||||||
for dep_domains in await resolved_domains_task:
|
# we plan on loading and promote them to stage 1
|
||||||
# Result is either a set or an exception. We ignore exceptions
|
deps_promotion = STAGE_1_INTEGRATIONS
|
||||||
# It will be properly handled during setup of the domain.
|
while deps_promotion:
|
||||||
if isinstance(dep_domains, set):
|
old_deps_promotion = deps_promotion
|
||||||
domains.update(dep_domains)
|
deps_promotion = set()
|
||||||
|
|
||||||
# setup components
|
for domain in old_deps_promotion:
|
||||||
logging_domains = domains & LOGGING_INTEGRATIONS
|
if domain not in domains_to_setup or domain in stage_1_domains:
|
||||||
stage_1_domains = domains & STAGE_1_INTEGRATIONS
|
continue
|
||||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
|
||||||
|
|
||||||
if logging_domains:
|
stage_1_domains.add(domain)
|
||||||
_LOGGER.info("Setting up %s", logging_domains)
|
|
||||||
|
|
||||||
await async_setup_multi_components(logging_domains)
|
dep_itg = integration_cache.get(domain)
|
||||||
|
|
||||||
|
if dep_itg is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
deps_promotion.update(dep_itg.all_dependencies)
|
||||||
|
|
||||||
|
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||||
|
|
||||||
# Kick off loading the registries. They don't need to be awaited.
|
# Kick off loading the registries. They don't need to be awaited.
|
||||||
asyncio.gather(
|
asyncio.gather(
|
||||||
@@ -405,49 +502,17 @@ async def _async_set_up_integrations(
|
|||||||
hass.helpers.area_registry.async_get_registry(),
|
hass.helpers.area_registry.async_get_registry(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Start setup
|
||||||
if stage_1_domains:
|
if stage_1_domains:
|
||||||
_LOGGER.info("Setting up %s", stage_1_domains)
|
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||||
|
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
|
||||||
|
|
||||||
await async_setup_multi_components(stage_1_domains)
|
# Enables after dependencies
|
||||||
|
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||||
|
|
||||||
# Load all integrations
|
|
||||||
after_dependencies: Dict[str, Set[str]] = {}
|
|
||||||
|
|
||||||
for int_or_exc in await asyncio.gather(
|
|
||||||
*(loader.async_get_integration(hass, domain) for domain in stage_2_domains),
|
|
||||||
return_exceptions=True,
|
|
||||||
):
|
|
||||||
# Exceptions are handled in async_setup_component.
|
|
||||||
if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies:
|
|
||||||
after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies)
|
|
||||||
|
|
||||||
last_load = None
|
|
||||||
while stage_2_domains:
|
|
||||||
domains_to_load = set()
|
|
||||||
|
|
||||||
for domain in stage_2_domains:
|
|
||||||
after_deps = after_dependencies.get(domain)
|
|
||||||
# Load if integration has no after_dependencies or they are
|
|
||||||
# all loaded
|
|
||||||
if not after_deps or not after_deps - hass.config.components:
|
|
||||||
domains_to_load.add(domain)
|
|
||||||
|
|
||||||
if not domains_to_load or domains_to_load == last_load:
|
|
||||||
break
|
|
||||||
|
|
||||||
_LOGGER.debug("Setting up %s", domains_to_load)
|
|
||||||
|
|
||||||
await async_setup_multi_components(domains_to_load)
|
|
||||||
|
|
||||||
last_load = domains_to_load
|
|
||||||
stage_2_domains -= domains_to_load
|
|
||||||
|
|
||||||
# These are stage 2 domains that never have their after_dependencies
|
|
||||||
# satisfied.
|
|
||||||
if stage_2_domains:
|
if stage_2_domains:
|
||||||
_LOGGER.debug("Final set up: %s", stage_2_domains)
|
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
||||||
|
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
|
||||||
await async_setup_multi_components(stage_2_domains)
|
|
||||||
|
|
||||||
# Wrap up startup
|
# Wrap up startup
|
||||||
_LOGGER.debug("Waiting for startup to wrap up")
|
_LOGGER.debug("Waiting for startup to wrap up")
|
||||||
|
@@ -37,7 +37,7 @@ def is_on(hass, entity_id=None):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not hasattr(component, "is_on"):
|
if not hasattr(component, "is_on"):
|
||||||
_LOGGER.warning("Integration %s has no is_on method.", domain)
|
_LOGGER.warning("Integration %s has no is_on method", domain)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if component.is_on(ent_id):
|
if component.is_on(ent_id):
|
||||||
|
@@ -4,5 +4,8 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||||
"requirements": ["abodepy==0.19.0"],
|
"requirements": ["abodepy==0.19.0"],
|
||||||
"codeowners": ["@shred86"]
|
"codeowners": ["@shred86"],
|
||||||
|
"homekit": {
|
||||||
|
"models": ["Abode", "Iota"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Abode"
|
|
||||||
}
|
|
@@ -61,7 +61,7 @@ class AcmedaCover(AcmedaBase, CoverEntity):
|
|||||||
None is unknown, 0 is closed, 100 is fully open.
|
None is unknown, 0 is closed, 100 is fully open.
|
||||||
"""
|
"""
|
||||||
position = None
|
position = None
|
||||||
if self.roller.type == 7 or self.roller.type == 10:
|
if self.roller.type in [7, 10]:
|
||||||
position = 100 - self.roller.closed_percent
|
position = 100 - self.roller.closed_percent
|
||||||
return position
|
return position
|
||||||
|
|
||||||
@@ -86,37 +86,36 @@ class AcmedaCover(AcmedaBase, CoverEntity):
|
|||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
"""Return if the cover is closed."""
|
"""Return if the cover is closed."""
|
||||||
is_closed = self.roller.closed_percent == 100
|
return self.roller.closed_percent == 100
|
||||||
return is_closed
|
|
||||||
|
|
||||||
async def close_cover(self, **kwargs):
|
async def async_close_cover(self, **kwargs):
|
||||||
"""Close the roller."""
|
"""Close the roller."""
|
||||||
await self.roller.move_down()
|
await self.roller.move_down()
|
||||||
|
|
||||||
async def open_cover(self, **kwargs):
|
async def async_open_cover(self, **kwargs):
|
||||||
"""Open the roller."""
|
"""Open the roller."""
|
||||||
await self.roller.move_up()
|
await self.roller.move_up()
|
||||||
|
|
||||||
async def stop_cover(self, **kwargs):
|
async def async_stop_cover(self, **kwargs):
|
||||||
"""Stop the roller."""
|
"""Stop the roller."""
|
||||||
await self.roller.move_stop()
|
await self.roller.move_stop()
|
||||||
|
|
||||||
async def set_cover_position(self, **kwargs):
|
async def async_set_cover_position(self, **kwargs):
|
||||||
"""Move the roller shutter to a specific position."""
|
"""Move the roller shutter to a specific position."""
|
||||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||||
|
|
||||||
async def close_cover_tilt(self, **kwargs):
|
async def async_close_cover_tilt(self, **kwargs):
|
||||||
"""Close the roller."""
|
"""Close the roller."""
|
||||||
await self.roller.move_down()
|
await self.roller.move_down()
|
||||||
|
|
||||||
async def open_cover_tilt(self, **kwargs):
|
async def async_open_cover_tilt(self, **kwargs):
|
||||||
"""Open the roller."""
|
"""Open the roller."""
|
||||||
await self.roller.move_up()
|
await self.roller.move_up()
|
||||||
|
|
||||||
async def stop_cover_tilt(self, **kwargs):
|
async def async_stop_cover_tilt(self, **kwargs):
|
||||||
"""Stop the roller."""
|
"""Stop the roller."""
|
||||||
await self.roller.move_stop()
|
await self.roller.move_stop()
|
||||||
|
|
||||||
async def set_cover_tilt(self, **kwargs):
|
async def async_set_cover_tilt(self, **kwargs):
|
||||||
"""Tilt the roller shutter to a specific position."""
|
"""Tilt the roller shutter to a specific position."""
|
||||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||||
|
@@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
|||||||
except AdGuardHomeConnectionError as exception:
|
except AdGuardHomeConnectionError as exception:
|
||||||
raise ConfigEntryNotReady from exception
|
raise ConfigEntryNotReady from exception
|
||||||
|
|
||||||
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
|
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"This integration requires AdGuard Home v0.99.0 or higher to work correctly"
|
"This integration requires AdGuard Home v0.99.0 or higher to work correctly"
|
||||||
)
|
)
|
||||||
@@ -183,7 +183,7 @@ class AdGuardHomeEntity(Entity):
|
|||||||
except AdGuardHomeError:
|
except AdGuardHomeError:
|
||||||
if self._available:
|
if self._available:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"An error occurred while updating AdGuard Home sensor.",
|
"An error occurred while updating AdGuard Home sensor",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
self._available = False
|
self._available = False
|
||||||
|
@@ -84,7 +84,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
|||||||
errors["base"] = "connection_error"
|
errors["base"] = "connection_error"
|
||||||
return await self._show_setup_form(errors)
|
return await self._show_setup_form(errors)
|
||||||
|
|
||||||
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
|
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
reason="adguard_home_outdated",
|
reason="adguard_home_outdated",
|
||||||
description_placeholders={
|
description_placeholders={
|
||||||
@@ -105,7 +105,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_hassio(self, user_input=None):
|
async def async_step_hassio(self, discovery_info):
|
||||||
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
||||||
|
|
||||||
This flow is triggered by the discovery component.
|
This flow is triggered by the discovery component.
|
||||||
@@ -113,14 +113,14 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
|||||||
entries = self._async_current_entries()
|
entries = self._async_current_entries()
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
self._hassio_discovery = user_input
|
self._hassio_discovery = discovery_info
|
||||||
return await self.async_step_hassio_confirm()
|
return await self.async_step_hassio_confirm()
|
||||||
|
|
||||||
cur_entry = entries[0]
|
cur_entry = entries[0]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cur_entry.data[CONF_HOST] == user_input[CONF_HOST]
|
cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST]
|
||||||
and cur_entry.data[CONF_PORT] == user_input[CONF_PORT]
|
and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT]
|
||||||
):
|
):
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
@@ -133,8 +133,8 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
|||||||
cur_entry,
|
cur_entry,
|
||||||
data={
|
data={
|
||||||
**cur_entry.data,
|
**cur_entry.data,
|
||||||
CONF_HOST: user_input[CONF_HOST],
|
CONF_HOST: discovery_info[CONF_HOST],
|
||||||
CONF_PORT: user_input[CONF_PORT],
|
CONF_PORT: discovery_info[CONF_PORT],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -73,7 +73,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
|
|||||||
try:
|
try:
|
||||||
await self._adguard_turn_off()
|
await self._adguard_turn_off()
|
||||||
except AdGuardHomeError:
|
except AdGuardHomeError:
|
||||||
_LOGGER.error("An error occurred while turning off AdGuard Home switch.")
|
_LOGGER.error("An error occurred while turning off AdGuard Home switch")
|
||||||
self._available = False
|
self._available = False
|
||||||
|
|
||||||
async def _adguard_turn_off(self) -> None:
|
async def _adguard_turn_off(self) -> None:
|
||||||
@@ -85,7 +85,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
|
|||||||
try:
|
try:
|
||||||
await self._adguard_turn_on()
|
await self._adguard_turn_on()
|
||||||
except AdGuardHomeError:
|
except AdGuardHomeError:
|
||||||
_LOGGER.error("An error occurred while turning on AdGuard Home switch.")
|
_LOGGER.error("An error occurred while turning on AdGuard Home switch")
|
||||||
self._available = False
|
self._available = False
|
||||||
|
|
||||||
async def _adguard_turn_on(self) -> None:
|
async def _adguard_turn_on(self) -> None:
|
||||||
|
@@ -4,6 +4,14 @@
|
|||||||
"hassio_confirm": {
|
"hassio_confirm": {
|
||||||
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?",
|
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?",
|
||||||
"title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
|
"title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Hostitel",
|
||||||
|
"password": "Heslo",
|
||||||
|
"port": "Port",
|
||||||
|
"username": "U\u017eivatelsk\u00e9 jm\u00e9no"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"connection_error": "Falha na liga\u00e7\u00e3o"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
"hassio_confirm": {
|
||||||
|
"title": "AdGuard Home via Hass.io add-on"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Servidor",
|
"host": "Servidor",
|
||||||
"password": "Palavra-passe",
|
"password": "Palavra-passe",
|
||||||
|
"port": "Porta",
|
||||||
"username": "Nome de Utilizador"
|
"username": "Nome de Utilizador"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,6 @@
|
|||||||
"domain": "ads",
|
"domain": "ads",
|
||||||
"name": "ADS",
|
"name": "ADS",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||||
"requirements": ["pyads==3.0.7"],
|
"requirements": ["pyads==3.1.3"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -74,8 +74,8 @@ class AgentCamera(MjpegCamera):
|
|||||||
|
|
||||||
device_info = {
|
device_info = {
|
||||||
CONF_NAME: device.name,
|
CONF_NAME: device.name,
|
||||||
CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size=640x480",
|
CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
|
||||||
CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size=640x480",
|
CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
|
||||||
}
|
}
|
||||||
self.device = device
|
self.device = device
|
||||||
self._removed = False
|
self._removed = False
|
||||||
|
@@ -23,13 +23,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Initialize the Agent config flow."""
|
"""Initialize the Agent config flow."""
|
||||||
self.device_config = {}
|
self.device_config = {}
|
||||||
|
|
||||||
async def async_step_user(self, info=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle an Agent config flow."""
|
"""Handle an Agent config flow."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if info is not None:
|
if user_input is not None:
|
||||||
host = info[CONF_HOST]
|
host = user_input[CONF_HOST]
|
||||||
port = info[CONF_PORT]
|
port = user_input[CONF_PORT]
|
||||||
|
|
||||||
server_origin = generate_url(host, port)
|
server_origin = generate_url(host, port)
|
||||||
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
|
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
|
||||||
@@ -48,8 +48,8 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
updates={
|
updates={
|
||||||
CONF_HOST: info[CONF_HOST],
|
CONF_HOST: user_input[CONF_HOST],
|
||||||
CONF_PORT: info[CONF_PORT],
|
CONF_PORT: user_input[CONF_PORT],
|
||||||
SERVER_URL: server_origin,
|
SERVER_URL: server_origin,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "agent_dvr",
|
"domain": "agent_dvr",
|
||||||
"name": "Agent DVR",
|
"name": "Agent DVR",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/",
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/",
|
||||||
"requirements": ["agent-py==0.0.20"],
|
"requirements": ["agent-py==0.0.23"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"codeowners": ["@ispysoftware"]
|
"codeowners": ["@ispysoftware"]
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"manual_confirm": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Host",
|
"host": "Hostitel",
|
||||||
"port": "Port"
|
"port": "Port"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -4,7 +4,7 @@
|
|||||||
"already_configured": "El dispositivo ya est\u00e1 configurado"
|
"already_configured": "El dispositivo ya est\u00e1 configurado"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"already_in_progress": "La configuraci\u00f3n del flujo para el dispositivo ya est\u00e1 en marcha.",
|
"already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en marcha.",
|
||||||
"device_unavailable": "El dispositivo no est\u00e1 disponible"
|
"device_unavailable": "El dispositivo no est\u00e1 disponible"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "H\u00f4te",
|
"host": "Nom d'h\u00f4te ou adresse IP",
|
||||||
"port": "Port"
|
"port": "Port"
|
||||||
},
|
},
|
||||||
"title": "Configurer l'agent DVR"
|
"title": "Configurer l'agent DVR"
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"enable_sensors": "Trafik sens\u00f6rleri ekleyin"
|
"api_key": "Kl\u00ed\u010d API"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -10,7 +10,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "Airly API-n\u00f8kkel",
|
"api_key": "API-n\u00f8kkel",
|
||||||
"latitude": "Breddegrad",
|
"latitude": "Breddegrad",
|
||||||
"longitude": "Lengdegrad",
|
"longitude": "Lengdegrad",
|
||||||
"name": "Navn p\u00e5 integrasjonen"
|
"name": "Navn p\u00e5 integrasjonen"
|
||||||
|
16
homeassistant/components/airvisual/translations/cs.json
Normal file
16
homeassistant/components/airvisual/translations/cs.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"geography": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Kl\u00ed\u010d API"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_pro": {
|
||||||
|
"data": {
|
||||||
|
"password": "Heslo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -21,7 +21,7 @@
|
|||||||
"node_pro": {
|
"node_pro": {
|
||||||
"data": {
|
"data": {
|
||||||
"ip_address": "Enhetens IP-adresse / vertsnavn",
|
"ip_address": "Enhetens IP-adresse / vertsnavn",
|
||||||
"password": "Passord for enhet"
|
"password": "Passord"
|
||||||
},
|
},
|
||||||
"description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.",
|
"description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.",
|
||||||
"title": "Konfigurer en AirVisual Node / Pro"
|
"title": "Konfigurer en AirVisual Node / Pro"
|
||||||
|
11
homeassistant/components/airvisual/translations/pt.json
Normal file
11
homeassistant/components/airvisual/translations/pt.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"node_pro": {
|
||||||
|
"data": {
|
||||||
|
"password": "Palavra-passe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
|
|
||||||
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -8,7 +8,7 @@ alarm_disarm:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to disarm the alarm control panel with.
|
description: An optional code to disarm the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
|
||||||
alarm_arm_custom_bypass:
|
alarm_arm_custom_bypass:
|
||||||
description: Send arm custom bypass command.
|
description: Send arm custom bypass command.
|
||||||
@@ -18,7 +18,7 @@ alarm_arm_custom_bypass:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to arm custom bypass the alarm control panel with.
|
description: An optional code to arm custom bypass the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
|
||||||
alarm_arm_home:
|
alarm_arm_home:
|
||||||
description: Send the alarm the command for arm home.
|
description: Send the alarm the command for arm home.
|
||||||
@@ -28,7 +28,7 @@ alarm_arm_home:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to arm home the alarm control panel with.
|
description: An optional code to arm home the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
|
||||||
alarm_arm_away:
|
alarm_arm_away:
|
||||||
description: Send the alarm the command for arm away.
|
description: Send the alarm the command for arm away.
|
||||||
@@ -38,7 +38,7 @@ alarm_arm_away:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to arm away the alarm control panel with.
|
description: An optional code to arm away the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
|
||||||
alarm_arm_night:
|
alarm_arm_night:
|
||||||
description: Send the alarm the command for arm night.
|
description: Send the alarm the command for arm night.
|
||||||
@@ -48,7 +48,7 @@ alarm_arm_night:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to arm night the alarm control panel with.
|
description: An optional code to arm night the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
|
||||||
alarm_trigger:
|
alarm_trigger:
|
||||||
description: Send the alarm the command for trigger.
|
description: Send the alarm the command for trigger.
|
||||||
@@ -58,4 +58,4 @@ alarm_trigger:
|
|||||||
example: "alarm_control_panel.downstairs"
|
example: "alarm_control_panel.downstairs"
|
||||||
code:
|
code:
|
||||||
description: An optional code to trigger the alarm control panel with.
|
description: An optional code to trigger the alarm control panel with.
|
||||||
example: 1234
|
example: "1234"
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"armed_away": "{entity_name} armada ausente",
|
"armed_away": "{entity_name} armada ausente",
|
||||||
"armed_home": "{entity_name} armada en casa",
|
"armed_home": "{entity_name} armada en casa",
|
||||||
"armed_night": "{entity_name} armada noche",
|
"armed_night": "{entity_name} armada noche",
|
||||||
"disarmed": "{entity_name} desarmado",
|
"disarmed": "{entity_name} desarmada",
|
||||||
"triggered": "{entity_name} activado"
|
"triggered": "{entity_name} activado"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from alarmdecoder import AlarmDecoder
|
from adext import AdExt
|
||||||
from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
|
from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
|
||||||
from alarmdecoder.util import NoDeviceError
|
from alarmdecoder.util import NoDeviceError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -162,7 +162,7 @@ def setup(hass, config):
|
|||||||
if not restart:
|
if not restart:
|
||||||
return
|
return
|
||||||
restart = False
|
restart = False
|
||||||
_LOGGER.warning("AlarmDecoder unexpectedly lost connection.")
|
_LOGGER.warning("AlarmDecoder unexpectedly lost connection")
|
||||||
hass.add_job(open_connection)
|
hass.add_job(open_connection)
|
||||||
|
|
||||||
def handle_message(sender, message):
|
def handle_message(sender, message):
|
||||||
@@ -189,13 +189,13 @@ def setup(hass, config):
|
|||||||
if device_type == "socket":
|
if device_type == "socket":
|
||||||
host = device[CONF_HOST]
|
host = device[CONF_HOST]
|
||||||
port = device[CONF_DEVICE_PORT]
|
port = device[CONF_DEVICE_PORT]
|
||||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
controller = AdExt(SocketDevice(interface=(host, port)))
|
||||||
elif device_type == "serial":
|
elif device_type == "serial":
|
||||||
path = device[CONF_DEVICE_PATH]
|
path = device[CONF_DEVICE_PATH]
|
||||||
baud = device[CONF_DEVICE_BAUD]
|
baud = device[CONF_DEVICE_BAUD]
|
||||||
controller = AlarmDecoder(SerialDevice(interface=path))
|
controller = AdExt(SerialDevice(interface=path))
|
||||||
elif device_type == "usb":
|
elif device_type == "usb":
|
||||||
AlarmDecoder(USBDevice.find())
|
AdExt(USBDevice.find())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
controller.on_message += handle_message
|
controller.on_message += handle_message
|
||||||
|
@@ -16,6 +16,7 @@ from homeassistant.const import (
|
|||||||
ATTR_CODE,
|
ATTR_CODE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
STATE_ALARM_DISARMED,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_TRIGGERED,
|
STATE_ALARM_TRIGGERED,
|
||||||
)
|
)
|
||||||
@@ -108,6 +109,8 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
|||||||
self._state = STATE_ALARM_TRIGGERED
|
self._state = STATE_ALARM_TRIGGERED
|
||||||
elif message.armed_away:
|
elif message.armed_away:
|
||||||
self._state = STATE_ALARM_ARMED_AWAY
|
self._state = STATE_ALARM_ARMED_AWAY
|
||||||
|
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
||||||
|
self._state = STATE_ALARM_ARMED_NIGHT
|
||||||
elif message.armed_home:
|
elif message.armed_home:
|
||||||
self._state = STATE_ALARM_ARMED_HOME
|
self._state = STATE_ALARM_ARMED_HOME
|
||||||
else:
|
else:
|
||||||
@@ -178,28 +181,27 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
|||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if code:
|
self.hass.data[DATA_AD].arm_away(
|
||||||
if self._auto_bypass:
|
code=code,
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}6#")
|
code_arm_required=self._code_arm_required,
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}2")
|
auto_bypass=self._auto_bypass,
|
||||||
elif not self._code_arm_required:
|
)
|
||||||
self.hass.data[DATA_AD].send("#2")
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if code:
|
self.hass.data[DATA_AD].arm_home(
|
||||||
if self._auto_bypass:
|
code=code,
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}6#")
|
code_arm_required=self._code_arm_required,
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}3")
|
auto_bypass=self._auto_bypass,
|
||||||
elif not self._code_arm_required:
|
)
|
||||||
self.hass.data[DATA_AD].send("#3")
|
|
||||||
|
|
||||||
def alarm_arm_night(self, code=None):
|
def alarm_arm_night(self, code=None):
|
||||||
"""Send arm night command."""
|
"""Send arm night command."""
|
||||||
if code:
|
self.hass.data[DATA_AD].arm_night(
|
||||||
self.hass.data[DATA_AD].send(f"{code!s}7")
|
code=code,
|
||||||
elif not self._code_arm_required:
|
code_arm_required=self._code_arm_required,
|
||||||
self.hass.data[DATA_AD].send("#7")
|
auto_bypass=self._auto_bypass,
|
||||||
|
)
|
||||||
|
|
||||||
def alarm_toggle_chime(self, code=None):
|
def alarm_toggle_chime(self, code=None):
|
||||||
"""Send toggle chime command."""
|
"""Send toggle chime command."""
|
||||||
|
@@ -2,6 +2,6 @@
|
|||||||
"domain": "alarmdecoder",
|
"domain": "alarmdecoder",
|
||||||
"name": "AlarmDecoder",
|
"name": "AlarmDecoder",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
|
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
|
||||||
"requirements": ["alarmdecoder==1.13.2"],
|
"requirements": ["adext==0.3"],
|
||||||
"codeowners": ["@ajschmidt8"]
|
"codeowners": ["@ajschmidt8"]
|
||||||
}
|
}
|
||||||
|
@@ -199,8 +199,8 @@ class Alert(ToggleEntity):
|
|||||||
self._send_done_message = False
|
self._send_done_message = False
|
||||||
self.entity_id = f"{DOMAIN}.{entity_id}"
|
self.entity_id = f"{DOMAIN}.{entity_id}"
|
||||||
|
|
||||||
event.async_track_state_change(
|
event.async_track_state_change_event(
|
||||||
hass, watched_entity_id, self.watched_entity_change
|
hass, [watched_entity_id], self.watched_entity_change
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -222,14 +222,12 @@ class Alert(ToggleEntity):
|
|||||||
return STATE_ON
|
return STATE_ON
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
|
|
||||||
@property
|
async def watched_entity_change(self, ev):
|
||||||
def hidden(self):
|
|
||||||
"""Hide the alert when it is not firing."""
|
|
||||||
return not self._can_ack or not self._firing
|
|
||||||
|
|
||||||
async def watched_entity_change(self, entity, from_state, to_state):
|
|
||||||
"""Determine if the alert should start or stop."""
|
"""Determine if the alert should start or stop."""
|
||||||
_LOGGER.debug("Watched entity (%s) has changed", entity)
|
to_state = ev.data.get("new_state")
|
||||||
|
if to_state is None:
|
||||||
|
return
|
||||||
|
_LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id"))
|
||||||
if to_state.state == self._alert_state and not self._firing:
|
if to_state.state == self._alert_state and not self._firing:
|
||||||
await self.begin_alerting()
|
await self.begin_alerting()
|
||||||
if to_state.state != self._alert_state and self._firing:
|
if to_state.state != self._alert_state and self._firing:
|
||||||
@@ -310,7 +308,9 @@ class Alert(ToggleEntity):
|
|||||||
_LOGGER.debug(msg_payload)
|
_LOGGER.debug(msg_payload)
|
||||||
|
|
||||||
for target in self._notifiers:
|
for target in self._notifiers:
|
||||||
await self.hass.services.async_call(DOMAIN_NOTIFY, target, msg_payload)
|
await self.hass.services.async_call(
|
||||||
|
DOMAIN_NOTIFY, target, msg_payload, context=self._context
|
||||||
|
)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
async def async_turn_on(self, **kwargs):
|
||||||
"""Async Unacknowledge alert."""
|
"""Async Unacknowledge alert."""
|
||||||
|
@@ -4,7 +4,6 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||||
|
|
||||||
from . import flash_briefings, intent, smart_home_http
|
from . import flash_briefings, intent, smart_home_http
|
||||||
@@ -17,12 +16,12 @@ from .const import (
|
|||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
CONF_FILTER,
|
CONF_FILTER,
|
||||||
CONF_LOCALE,
|
CONF_LOCALE,
|
||||||
|
CONF_PASSWORD,
|
||||||
CONF_SUPPORTED_LOCALES,
|
CONF_SUPPORTED_LOCALES,
|
||||||
CONF_TEXT,
|
CONF_TEXT,
|
||||||
CONF_TITLE,
|
CONF_TITLE,
|
||||||
CONF_UID,
|
CONF_UID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_ALEXA_SMART_HOME,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -56,6 +55,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
{
|
{
|
||||||
DOMAIN: {
|
DOMAIN: {
|
||||||
CONF_FLASH_BRIEFINGS: {
|
CONF_FLASH_BRIEFINGS: {
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
cv.string: vol.All(
|
cv.string: vol.All(
|
||||||
cv.ensure_list,
|
cv.ensure_list,
|
||||||
[
|
[
|
||||||
@@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
# vol.Optional here would mean we couldn't distinguish between an empty
|
# vol.Optional here would mean we couldn't distinguish between an empty
|
||||||
# smart_home: and none at all.
|
# smart_home: and none at all.
|
||||||
@@ -80,28 +80,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Activate the Alexa component."""
|
"""Activate the Alexa component."""
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_describe_logbook_event(event):
|
|
||||||
"""Describe a logbook event."""
|
|
||||||
data = event.data
|
|
||||||
entity_id = data["request"].get("entity_id")
|
|
||||||
|
|
||||||
if entity_id:
|
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
name = state.name if state else entity_id
|
|
||||||
message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}"
|
|
||||||
else:
|
|
||||||
message = (
|
|
||||||
f"send command {data['request']['namespace']}/{data['request']['name']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id}
|
|
||||||
|
|
||||||
hass.components.logbook.async_describe_event(
|
|
||||||
DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event
|
|
||||||
)
|
|
||||||
|
|
||||||
if DOMAIN not in config:
|
if DOMAIN not in config:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@@ -70,11 +70,11 @@ class Auth:
|
|||||||
await self.async_load_preferences()
|
await self.async_load_preferences()
|
||||||
|
|
||||||
if self.is_token_valid():
|
if self.is_token_valid():
|
||||||
_LOGGER.debug("Token still valid, using it.")
|
_LOGGER.debug("Token still valid, using it")
|
||||||
return self._prefs[STORAGE_ACCESS_TOKEN]
|
return self._prefs[STORAGE_ACCESS_TOKEN]
|
||||||
|
|
||||||
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
||||||
_LOGGER.debug("Token invalid and no refresh token available.")
|
_LOGGER.debug("Token invalid and no refresh token available")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
lwa_params = {
|
lwa_params = {
|
||||||
@@ -84,7 +84,7 @@ class Auth:
|
|||||||
CONF_CLIENT_SECRET: self.client_secret,
|
CONF_CLIENT_SECRET: self.client_secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER.debug("Calling LWA to refresh the access token.")
|
_LOGGER.debug("Calling LWA to refresh the access token")
|
||||||
return await self._async_request_new_token(lwa_params)
|
return await self._async_request_new_token(lwa_params)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -113,14 +113,14 @@ class Auth:
|
|||||||
)
|
)
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Timeout calling LWA to get auth token.")
|
_LOGGER.error("Timeout calling LWA to get auth token")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_LOGGER.debug("LWA response header: %s", response.headers)
|
_LOGGER.debug("LWA response header: %s", response.headers)
|
||||||
_LOGGER.debug("LWA response status: %s", response.status)
|
_LOGGER.debug("LWA response status: %s", response.status)
|
||||||
|
|
||||||
if response.status != HTTP_OK:
|
if response.status != HTTP_OK:
|
||||||
_LOGGER.error("Error calling LWA to get auth token.")
|
_LOGGER.error("Error calling LWA to get auth token")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
response_json = await response.json()
|
response_json = await response.json()
|
||||||
|
@@ -19,6 +19,7 @@ CONF_FILTER = "filter"
|
|||||||
CONF_ENTITY_CONFIG = "entity_config"
|
CONF_ENTITY_CONFIG = "entity_config"
|
||||||
CONF_ENDPOINT = "endpoint"
|
CONF_ENDPOINT = "endpoint"
|
||||||
CONF_LOCALE = "locale"
|
CONF_LOCALE = "locale"
|
||||||
|
CONF_PASSWORD = "password"
|
||||||
|
|
||||||
ATTR_UID = "uid"
|
ATTR_UID = "uid"
|
||||||
ATTR_UPDATE_DATE = "updateDate"
|
ATTR_UPDATE_DATE = "updateDate"
|
||||||
@@ -39,6 +40,7 @@ API_HEADER = "header"
|
|||||||
API_PAYLOAD = "payload"
|
API_PAYLOAD = "payload"
|
||||||
API_SCOPE = "scope"
|
API_SCOPE = "scope"
|
||||||
API_CHANGE = "change"
|
API_CHANGE = "change"
|
||||||
|
API_PASSWORD = "password"
|
||||||
|
|
||||||
CONF_DESCRIPTION = "description"
|
CONF_DESCRIPTION = "description"
|
||||||
CONF_DISPLAY_CATEGORIES = "display_categories"
|
CONF_DISPLAY_CATEGORIES = "display_categories"
|
||||||
|
@@ -590,9 +590,8 @@ class ScriptCapabilities(AlexaEntity):
|
|||||||
|
|
||||||
def interfaces(self):
|
def interfaces(self):
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
can_cancel = bool(self.entity.attributes.get("can_cancel"))
|
|
||||||
return [
|
return [
|
||||||
AlexaSceneController(self.entity, supports_deactivation=can_cancel),
|
AlexaSceneController(self.entity, supports_deactivation=True),
|
||||||
Alexa(self.hass),
|
Alexa(self.hass),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
"""Support for Alexa skill service end point."""
|
"""Support for Alexa skill service end point."""
|
||||||
import copy
|
import copy
|
||||||
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from homeassistant.components import http
|
from homeassistant.components import http
|
||||||
from homeassistant.const import HTTP_NOT_FOUND
|
from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
API_PASSWORD,
|
||||||
ATTR_MAIN_TEXT,
|
ATTR_MAIN_TEXT,
|
||||||
ATTR_REDIRECTION_URL,
|
ATTR_REDIRECTION_URL,
|
||||||
ATTR_STREAM_URL,
|
ATTR_STREAM_URL,
|
||||||
@@ -18,6 +20,7 @@ from .const import (
|
|||||||
ATTR_UPDATE_DATE,
|
ATTR_UPDATE_DATE,
|
||||||
CONF_AUDIO,
|
CONF_AUDIO,
|
||||||
CONF_DISPLAY_URL,
|
CONF_DISPLAY_URL,
|
||||||
|
CONF_PASSWORD,
|
||||||
CONF_TEXT,
|
CONF_TEXT,
|
||||||
CONF_TITLE,
|
CONF_TITLE,
|
||||||
CONF_UID,
|
CONF_UID,
|
||||||
@@ -39,6 +42,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
|||||||
"""Handle Alexa Flash Briefing skill requests."""
|
"""Handle Alexa Flash Briefing skill requests."""
|
||||||
|
|
||||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||||
|
requires_auth = False
|
||||||
name = "api:alexa:flash_briefings"
|
name = "api:alexa:flash_briefings"
|
||||||
|
|
||||||
def __init__(self, hass, flash_briefings):
|
def __init__(self, hass, flash_briefings):
|
||||||
@@ -52,7 +56,20 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
|||||||
"""Handle Alexa Flash Briefing request."""
|
"""Handle Alexa Flash Briefing request."""
|
||||||
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
||||||
|
|
||||||
if self.flash_briefings.get(briefing_id) is None:
|
if request.query.get(API_PASSWORD) is None:
|
||||||
|
err = "No password provided for Alexa flash briefing: %s"
|
||||||
|
_LOGGER.error(err, briefing_id)
|
||||||
|
return b"", HTTP_UNAUTHORIZED
|
||||||
|
|
||||||
|
if not hmac.compare_digest(
|
||||||
|
request.query[API_PASSWORD].encode("utf-8"),
|
||||||
|
self.flash_briefings[CONF_PASSWORD].encode("utf-8"),
|
||||||
|
):
|
||||||
|
err = "Wrong password for Alexa flash briefing: %s"
|
||||||
|
_LOGGER.error(err, briefing_id)
|
||||||
|
return b"", HTTP_UNAUTHORIZED
|
||||||
|
|
||||||
|
if not isinstance(self.flash_briefings.get(briefing_id), list):
|
||||||
err = "No configured Alexa flash briefing was found for: %s"
|
err = "No configured Alexa flash briefing was found for: %s"
|
||||||
_LOGGER.error(err, briefing_id)
|
_LOGGER.error(err, briefing_id)
|
||||||
return b"", HTTP_NOT_FOUND
|
return b"", HTTP_NOT_FOUND
|
||||||
|
28
homeassistant/components/alexa/logbook.py
Normal file
28
homeassistant/components/alexa/logbook.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Describe logbook events."""
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_describe_events(hass, async_describe_event):
|
||||||
|
"""Describe logbook events."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_describe_logbook_event(event):
|
||||||
|
"""Describe a logbook event."""
|
||||||
|
data = event.data
|
||||||
|
entity_id = data["request"].get("entity_id")
|
||||||
|
|
||||||
|
if entity_id:
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
name = state.name if state else entity_id
|
||||||
|
message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}"
|
||||||
|
else:
|
||||||
|
message = (
|
||||||
|
f"send command {data['request']['namespace']}/{data['request']['name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id}
|
||||||
|
|
||||||
|
async_describe_event(DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event)
|
@@ -2,7 +2,14 @@
|
|||||||
"domain": "alexa",
|
"domain": "alexa",
|
||||||
"name": "Amazon Alexa",
|
"name": "Amazon Alexa",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||||
"dependencies": ["http"],
|
"dependencies": [
|
||||||
"after_dependencies": ["logbook", "camera"],
|
"http"
|
||||||
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
|
],
|
||||||
|
"after_dependencies": [
|
||||||
|
"camera"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@home-assistant/cloud",
|
||||||
|
"@ochlocracy"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -101,7 +101,7 @@ async def async_send_changereport_message(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Timeout sending report to Alexa.")
|
_LOGGER.error("Timeout sending report to Alexa")
|
||||||
return
|
return
|
||||||
|
|
||||||
response_text = await response.text()
|
response_text = await response.text()
|
||||||
@@ -233,7 +233,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Timeout sending report to Alexa.")
|
_LOGGER.error("Timeout sending report to Alexa")
|
||||||
return
|
return
|
||||||
|
|
||||||
response_text = await response.text()
|
response_text = await response.text()
|
||||||
|
@@ -94,12 +94,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
|
|||||||
data={"type": TYPE_LOCAL, "host": user_input["host"]},
|
data={"type": TYPE_LOCAL, "host": user_input["host"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_hassio(self, user_input=None):
|
async def async_step_hassio(self, discovery_info):
|
||||||
"""Receive a Hass.io discovery."""
|
"""Receive a Hass.io discovery."""
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
return self.async_abort(reason="already_setup")
|
return self.async_abort(reason="already_setup")
|
||||||
|
|
||||||
self.hassio_discovery = user_input
|
self.hassio_discovery = discovery_info
|
||||||
|
|
||||||
return await self.async_step_hassio_confirm()
|
return await self.async_step_hassio_confirm()
|
||||||
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Almond"
|
|
||||||
}
|
|
@@ -3,5 +3,5 @@
|
|||||||
"name": "Amazon Polly",
|
"name": "Amazon Polly",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
|
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
|
||||||
"requirements": ["boto3==1.9.252"],
|
"requirements": ["boto3==1.9.252"],
|
||||||
"codeowners": ["@robbiet480"]
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Ambiclimate"
|
|
||||||
}
|
|
@@ -10,6 +10,7 @@ from homeassistant.config_entries import SOURCE_IMPORT
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LOCATION,
|
ATTR_LOCATION,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
DEGREE,
|
DEGREE,
|
||||||
@@ -126,6 +127,8 @@ TYPE_TEMPF = "tempf"
|
|||||||
TYPE_TEMPINF = "tempinf"
|
TYPE_TEMPINF = "tempinf"
|
||||||
TYPE_TOTALRAININ = "totalrainin"
|
TYPE_TOTALRAININ = "totalrainin"
|
||||||
TYPE_UV = "uv"
|
TYPE_UV = "uv"
|
||||||
|
TYPE_PM25 = "pm25"
|
||||||
|
TYPE_PM25_24H = "pm25_24h"
|
||||||
TYPE_WEEKLYRAININ = "weeklyrainin"
|
TYPE_WEEKLYRAININ = "weeklyrainin"
|
||||||
TYPE_WINDDIR = "winddir"
|
TYPE_WINDDIR = "winddir"
|
||||||
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
|
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
|
||||||
@@ -218,6 +221,13 @@ SENSOR_TYPES = {
|
|||||||
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
|
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
|
||||||
TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None),
|
TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_UV: ("uv", "Index", TYPE_SENSOR, None),
|
TYPE_UV: ("uv", "Index", TYPE_SENSOR, None),
|
||||||
|
TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TYPE_SENSOR, None),
|
||||||
|
TYPE_PM25_24H: (
|
||||||
|
"PM25 24h Avg",
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
TYPE_SENSOR,
|
||||||
|
None,
|
||||||
|
),
|
||||||
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
|
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
|
||||||
TYPE_WINDDIR: ("Wind Dir", DEGREE, TYPE_SENSOR, None),
|
TYPE_WINDDIR: ("Wind Dir", DEGREE, TYPE_SENSOR, None),
|
||||||
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, TYPE_SENSOR, None),
|
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, TYPE_SENSOR, None),
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "Kl\u00ed\u010d API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Ambient PWS"
|
|
||||||
}
|
|
@@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s
|
|||||||
from homeassistant.helpers.event import track_time_interval
|
from homeassistant.helpers.event import track_time_interval
|
||||||
from homeassistant.helpers.service import async_extract_entity_ids
|
from homeassistant.helpers.service import async_extract_entity_ids
|
||||||
|
|
||||||
from .binary_sensor import BINARY_SENSORS
|
from .binary_sensor import BINARY_POLLED_SENSORS, BINARY_SENSORS, check_binary_sensors
|
||||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||||
from .const import (
|
from .const import (
|
||||||
CAMERAS,
|
CAMERAS,
|
||||||
@@ -98,7 +98,7 @@ AMCREST_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
|
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
|
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
|
||||||
vol.Optional(CONF_BINARY_SENSORS): vol.All(
|
vol.Optional(CONF_BINARY_SENSORS): vol.All(
|
||||||
cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique()
|
cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique(), check_binary_sensors
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_SENSORS): vol.All(
|
vol.Optional(CONF_SENSORS): vol.All(
|
||||||
cv.ensure_list, [vol.In(SENSORS)], vol.Unique()
|
cv.ensure_list, [vol.In(SENSORS)], vol.Unique()
|
||||||
@@ -271,7 +271,7 @@ def setup(hass, config):
|
|||||||
event_codes = [
|
event_codes = [
|
||||||
BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE]
|
BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE]
|
||||||
for sensor_type in binary_sensors
|
for sensor_type in binary_sensors
|
||||||
if BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] is not None
|
if sensor_type not in BINARY_POLLED_SENSORS
|
||||||
]
|
]
|
||||||
if event_codes:
|
if event_codes:
|
||||||
_start_event_monitor(hass, name, api, event_codes)
|
_start_event_monitor(hass, name, api, event_codes)
|
||||||
|
@@ -3,15 +3,18 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from amcrest import AmcrestError
|
from amcrest import AmcrestError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASS_CONNECTIVITY,
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
DEVICE_CLASS_MOTION,
|
DEVICE_CLASS_MOTION,
|
||||||
|
DEVICE_CLASS_SOUND,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME
|
from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
BINARY_SENSOR_SCAN_INTERVAL_SECS,
|
BINARY_SENSOR_SCAN_INTERVAL_SECS,
|
||||||
@@ -28,25 +31,48 @@ from .helpers import log_update_error, service_signal
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
||||||
|
_ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
||||||
|
|
||||||
|
BINARY_SENSOR_AUDIO_DETECTED = "audio_detected"
|
||||||
|
BINARY_SENSOR_AUDIO_DETECTED_POLLED = "audio_detected_polled"
|
||||||
BINARY_SENSOR_MOTION_DETECTED = "motion_detected"
|
BINARY_SENSOR_MOTION_DETECTED = "motion_detected"
|
||||||
|
BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled"
|
||||||
BINARY_SENSOR_ONLINE = "online"
|
BINARY_SENSOR_ONLINE = "online"
|
||||||
|
BINARY_POLLED_SENSORS = [
|
||||||
|
BINARY_SENSOR_AUDIO_DETECTED_POLLED,
|
||||||
|
BINARY_SENSOR_MOTION_DETECTED_POLLED,
|
||||||
|
BINARY_SENSOR_ONLINE,
|
||||||
|
]
|
||||||
|
_AUDIO_DETECTED_PARAMS = ("Audio Detected", DEVICE_CLASS_SOUND, "AudioMutation")
|
||||||
|
_MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion")
|
||||||
BINARY_SENSORS = {
|
BINARY_SENSORS = {
|
||||||
BINARY_SENSOR_MOTION_DETECTED: (
|
BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS,
|
||||||
"Motion Detected",
|
BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS,
|
||||||
DEVICE_CLASS_MOTION,
|
BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS,
|
||||||
"VideoMotion",
|
BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS,
|
||||||
),
|
|
||||||
BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None),
|
BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None),
|
||||||
}
|
}
|
||||||
BINARY_SENSORS = {
|
BINARY_SENSORS = {
|
||||||
k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v))
|
k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v))
|
||||||
for k, v in BINARY_SENSORS.items()
|
for k, v in BINARY_SENSORS.items()
|
||||||
}
|
}
|
||||||
|
_EXCLUSIVE_OPTIONS = [
|
||||||
|
{BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED},
|
||||||
|
]
|
||||||
|
|
||||||
_UPDATE_MSG = "Updating %s binary sensor"
|
_UPDATE_MSG = "Updating %s binary sensor"
|
||||||
|
|
||||||
|
|
||||||
|
def check_binary_sensors(value):
|
||||||
|
"""Validate binary sensor configurations."""
|
||||||
|
for exclusive_options in _EXCLUSIVE_OPTIONS:
|
||||||
|
if len(set(value) & exclusive_options) > 1:
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"must contain at most one of {', '.join(exclusive_options)}."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up a binary sensor for an Amcrest IP Camera."""
|
"""Set up a binary sensor for an Amcrest IP Camera."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
@@ -80,7 +106,7 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
|||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Return True if entity has to be polled for state."""
|
"""Return True if entity has to be polled for state."""
|
||||||
return self._sensor_type == BINARY_SENSOR_ONLINE
|
return self._sensor_type in BINARY_POLLED_SENSORS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -109,6 +135,7 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
|||||||
else:
|
else:
|
||||||
self._update_others()
|
self._update_others()
|
||||||
|
|
||||||
|
@Throttle(_ONLINE_SCAN_INTERVAL)
|
||||||
def _update_online(self):
|
def _update_online(self):
|
||||||
if not (self._api.available or self.is_on):
|
if not (self._api.available or self.is_on):
|
||||||
return
|
return
|
||||||
@@ -137,6 +164,11 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
|||||||
|
|
||||||
async def async_on_demand_update(self):
|
async def async_on_demand_update(self):
|
||||||
"""Update state."""
|
"""Update state."""
|
||||||
|
if self._sensor_type == BINARY_SENSOR_ONLINE:
|
||||||
|
_LOGGER.debug(_UPDATE_MSG, self._name)
|
||||||
|
self._state = self._api.available
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
self.async_schedule_update_ha_state(True)
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -155,7 +187,7 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
|||||||
self.async_on_demand_update,
|
self.async_on_demand_update,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self._event_code:
|
if self._event_code and self._sensor_type not in BINARY_POLLED_SENSORS:
|
||||||
self._unsub_dispatcher.append(
|
self._unsub_dispatcher.append(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@@ -130,6 +130,10 @@ class CannotSnapshot(Exception):
|
|||||||
"""Conditions are not valid for taking a snapshot."""
|
"""Conditions are not valid for taking a snapshot."""
|
||||||
|
|
||||||
|
|
||||||
|
class AmcrestCommandFailed(Exception):
|
||||||
|
"""Amcrest camera command did not work."""
|
||||||
|
|
||||||
|
|
||||||
class AmcrestCam(Camera):
|
class AmcrestCam(Camera):
|
||||||
"""An implementation of an Amcrest IP camera."""
|
"""An implementation of an Amcrest IP camera."""
|
||||||
|
|
||||||
@@ -367,12 +371,12 @@ class AmcrestCam(Camera):
|
|||||||
self._model = resp.split("=")[-1]
|
self._model = resp.split("=")[-1]
|
||||||
else:
|
else:
|
||||||
self._model = "unknown"
|
self._model = "unknown"
|
||||||
self.is_streaming = self._api.video_enabled
|
self.is_streaming = self._get_video()
|
||||||
self._is_recording = self._api.record_mode == "Manual"
|
self._is_recording = self._get_recording()
|
||||||
self._motion_detection_enabled = self._api.is_motion_detector_on()
|
self._motion_detection_enabled = self._get_motion_detection()
|
||||||
self._audio_enabled = self._api.audio_enabled
|
self._audio_enabled = self._get_audio()
|
||||||
self._motion_recording_enabled = self._api.is_record_on_motion_detection()
|
self._motion_recording_enabled = self._get_motion_recording()
|
||||||
self._color_bw = _CBW[self._api.day_night_color]
|
self._color_bw = self._get_color_mode()
|
||||||
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
|
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
|
||||||
except AmcrestError as error:
|
except AmcrestError as error:
|
||||||
log_update_error(_LOGGER, "get", self.name, "camera attributes", error)
|
log_update_error(_LOGGER, "get", self.name, "camera attributes", error)
|
||||||
@@ -384,11 +388,11 @@ class AmcrestCam(Camera):
|
|||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off camera."""
|
"""Turn off camera."""
|
||||||
self._enable_video_stream(False)
|
self._enable_video(False)
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn on camera."""
|
"""Turn on camera."""
|
||||||
self._enable_video_stream(True)
|
self._enable_video(True)
|
||||||
|
|
||||||
def enable_motion_detection(self):
|
def enable_motion_detection(self):
|
||||||
"""Enable motion detection in the camera."""
|
"""Enable motion detection in the camera."""
|
||||||
@@ -465,28 +469,53 @@ class AmcrestCam(Camera):
|
|||||||
|
|
||||||
# Methods to send commands to Amcrest camera and handle errors
|
# Methods to send commands to Amcrest camera and handle errors
|
||||||
|
|
||||||
def _enable_video_stream(self, enable):
|
def _change_setting(self, value, attr, description, action="set"):
|
||||||
|
func = description.replace(" ", "_")
|
||||||
|
description = f"camera {description} to {value}"
|
||||||
|
tries = 3
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
getattr(self, f"_set_{func}")(value)
|
||||||
|
new_value = getattr(self, f"_get_{func}")()
|
||||||
|
if new_value != value:
|
||||||
|
raise AmcrestCommandFailed
|
||||||
|
except (AmcrestError, AmcrestCommandFailed) as error:
|
||||||
|
if tries == 1:
|
||||||
|
log_update_error(_LOGGER, action, self.name, description, error)
|
||||||
|
return
|
||||||
|
log_update_error(
|
||||||
|
_LOGGER, action, self.name, description, error, logging.DEBUG
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if attr:
|
||||||
|
setattr(self, attr, new_value)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
return
|
||||||
|
tries -= 1
|
||||||
|
|
||||||
|
def _get_video(self):
|
||||||
|
return self._api.video_enabled
|
||||||
|
|
||||||
|
def _set_video(self, enable):
|
||||||
|
self._api.video_enabled = enable
|
||||||
|
|
||||||
|
def _enable_video(self, enable):
|
||||||
"""Enable or disable camera video stream."""
|
"""Enable or disable camera video stream."""
|
||||||
# Given the way the camera's state is determined by
|
# Given the way the camera's state is determined by
|
||||||
# is_streaming and is_recording, we can't leave
|
# is_streaming and is_recording, we can't leave
|
||||||
# recording on if video stream is being turned off.
|
# recording on if video stream is being turned off.
|
||||||
if self.is_recording and not enable:
|
if self.is_recording and not enable:
|
||||||
self._enable_recording(False)
|
self._enable_recording(False)
|
||||||
try:
|
self._change_setting(enable, "is_streaming", "video")
|
||||||
self._api.video_enabled = enable
|
|
||||||
except AmcrestError as error:
|
|
||||||
log_update_error(
|
|
||||||
_LOGGER,
|
|
||||||
"enable" if enable else "disable",
|
|
||||||
self.name,
|
|
||||||
"camera video stream",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.is_streaming = enable
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
if self._control_light:
|
if self._control_light:
|
||||||
self._enable_light(self._audio_enabled or self.is_streaming)
|
self._change_light()
|
||||||
|
|
||||||
|
def _get_recording(self):
|
||||||
|
return self._api.record_mode == "Manual"
|
||||||
|
|
||||||
|
def _set_recording(self, enable):
|
||||||
|
rec_mode = {"Automatic": 0, "Manual": 1}
|
||||||
|
self._api.record_mode = rec_mode["Manual" if enable else "Automatic"]
|
||||||
|
|
||||||
def _enable_recording(self, enable):
|
def _enable_recording(self, enable):
|
||||||
"""Turn recording on or off."""
|
"""Turn recording on or off."""
|
||||||
@@ -494,86 +523,56 @@ class AmcrestCam(Camera):
|
|||||||
# is_streaming and is_recording, we can't leave
|
# is_streaming and is_recording, we can't leave
|
||||||
# video stream off if recording is being turned on.
|
# video stream off if recording is being turned on.
|
||||||
if not self.is_streaming and enable:
|
if not self.is_streaming and enable:
|
||||||
self._enable_video_stream(True)
|
self._enable_video(True)
|
||||||
rec_mode = {"Automatic": 0, "Manual": 1}
|
self._change_setting(enable, "_is_recording", "recording")
|
||||||
try:
|
|
||||||
self._api.record_mode = rec_mode["Manual" if enable else "Automatic"]
|
def _get_motion_detection(self):
|
||||||
except AmcrestError as error:
|
return self._api.is_motion_detector_on()
|
||||||
log_update_error(
|
|
||||||
_LOGGER,
|
def _set_motion_detection(self, enable):
|
||||||
"enable" if enable else "disable",
|
self._api.motion_detection = str(enable).lower()
|
||||||
self.name,
|
|
||||||
"camera recording",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._is_recording = enable
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def _enable_motion_detection(self, enable):
|
def _enable_motion_detection(self, enable):
|
||||||
"""Enable or disable motion detection."""
|
"""Enable or disable motion detection."""
|
||||||
try:
|
self._change_setting(enable, "_motion_detection_enabled", "motion detection")
|
||||||
self._api.motion_detection = str(enable).lower()
|
|
||||||
except AmcrestError as error:
|
def _get_audio(self):
|
||||||
log_update_error(
|
return self._api.audio_enabled
|
||||||
_LOGGER,
|
|
||||||
"enable" if enable else "disable",
|
def _set_audio(self, enable):
|
||||||
self.name,
|
self._api.audio_enabled = enable
|
||||||
"camera motion detection",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._motion_detection_enabled = enable
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def _enable_audio(self, enable):
|
def _enable_audio(self, enable):
|
||||||
"""Enable or disable audio stream."""
|
"""Enable or disable audio stream."""
|
||||||
try:
|
self._change_setting(enable, "_audio_enabled", "audio")
|
||||||
self._api.audio_enabled = enable
|
|
||||||
except AmcrestError as error:
|
|
||||||
log_update_error(
|
|
||||||
_LOGGER,
|
|
||||||
"enable" if enable else "disable",
|
|
||||||
self.name,
|
|
||||||
"camera audio stream",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._audio_enabled = enable
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
if self._control_light:
|
if self._control_light:
|
||||||
self._enable_light(self._audio_enabled or self.is_streaming)
|
self._change_light()
|
||||||
|
|
||||||
def _enable_light(self, enable):
|
def _get_indicator_light(self):
|
||||||
|
return "true" in self._api.command(
|
||||||
|
"configManager.cgi?action=getConfig&name=LightGlobal"
|
||||||
|
).content.decode("utf-8")
|
||||||
|
|
||||||
|
def _set_indicator_light(self, enable):
|
||||||
|
self._api.command(
|
||||||
|
f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _change_light(self):
|
||||||
"""Enable or disable indicator light."""
|
"""Enable or disable indicator light."""
|
||||||
try:
|
self._change_setting(
|
||||||
self._api.command(
|
self._audio_enabled or self.is_streaming, None, "indicator light"
|
||||||
f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
|
)
|
||||||
)
|
|
||||||
except AmcrestError as error:
|
def _get_motion_recording(self):
|
||||||
log_update_error(
|
return self._api.is_record_on_motion_detection()
|
||||||
_LOGGER,
|
|
||||||
"enable" if enable else "disable",
|
def _set_motion_recording(self, enable):
|
||||||
self.name,
|
self._api.motion_recording = str(enable).lower()
|
||||||
"indicator light",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _enable_motion_recording(self, enable):
|
def _enable_motion_recording(self, enable):
|
||||||
"""Enable or disable motion recording."""
|
"""Enable or disable motion recording."""
|
||||||
try:
|
self._change_setting(enable, "_motion_recording_enabled", "motion recording")
|
||||||
self._api.motion_recording = str(enable).lower()
|
|
||||||
except AmcrestError as error:
|
|
||||||
log_update_error(
|
|
||||||
_LOGGER,
|
|
||||||
"enable" if enable else "disable",
|
|
||||||
self.name,
|
|
||||||
"camera motion recording",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._motion_recording_enabled = enable
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def _goto_preset(self, preset):
|
def _goto_preset(self, preset):
|
||||||
"""Move camera position and zoom to preset."""
|
"""Move camera position and zoom to preset."""
|
||||||
@@ -584,17 +583,15 @@ class AmcrestCam(Camera):
|
|||||||
_LOGGER, "move", self.name, f"camera to preset {preset}", error
|
_LOGGER, "move", self.name, f"camera to preset {preset}", error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_color_mode(self):
|
||||||
|
return _CBW[self._api.day_night_color]
|
||||||
|
|
||||||
|
def _set_color_mode(self, cbw):
|
||||||
|
self._api.day_night_color = _CBW.index(cbw)
|
||||||
|
|
||||||
def _set_color_bw(self, cbw):
|
def _set_color_bw(self, cbw):
|
||||||
"""Set camera color mode."""
|
"""Set camera color mode."""
|
||||||
try:
|
self._change_setting(cbw, "_color_bw", "color mode")
|
||||||
self._api.day_night_color = _CBW.index(cbw)
|
|
||||||
except AmcrestError as error:
|
|
||||||
log_update_error(
|
|
||||||
_LOGGER, "set", self.name, f"camera color mode to {cbw}", error
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._color_bw = cbw
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
def _start_tour(self, start):
|
def _start_tour(self, start):
|
||||||
"""Start camera tour."""
|
"""Start camera tour."""
|
||||||
|
@@ -4,7 +4,7 @@ DATA_AMCREST = DOMAIN
|
|||||||
CAMERAS = "cameras"
|
CAMERAS = "cameras"
|
||||||
DEVICES = "devices"
|
DEVICES = "devices"
|
||||||
|
|
||||||
BINARY_SENSOR_SCAN_INTERVAL_SECS = 60
|
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
|
||||||
CAMERA_WEB_SESSION_TIMEOUT = 10
|
CAMERA_WEB_SESSION_TIMEOUT = 10
|
||||||
COMM_RETRIES = 1
|
COMM_RETRIES = 1
|
||||||
COMM_TIMEOUT = 6.05
|
COMM_TIMEOUT = 6.05
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
"""Helpers for amcrest component."""
|
"""Helpers for amcrest component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@@ -7,9 +9,10 @@ def service_signal(service, *args):
|
|||||||
return "_".join([DOMAIN, service, *args])
|
return "_".join([DOMAIN, service, *args])
|
||||||
|
|
||||||
|
|
||||||
def log_update_error(logger, action, name, entity_type, error):
|
def log_update_error(logger, action, name, entity_type, error, level=logging.ERROR):
|
||||||
"""Log an update error."""
|
"""Log an update error."""
|
||||||
logger.error(
|
logger.log(
|
||||||
|
level,
|
||||||
"Could not %s %s %s due to error: %s",
|
"Could not %s %s %s due to error: %s",
|
||||||
action,
|
action,
|
||||||
name,
|
name,
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
"name": "Android TV",
|
"name": "Android TV",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"adb-shell==0.1.3",
|
"adb-shell[async]==0.2.0",
|
||||||
"androidtv==0.0.41",
|
"androidtv[async]==0.0.46",
|
||||||
"pure-python-adb==0.2.2.dev0"
|
"pure-python-adb==0.2.2.dev0"
|
||||||
],
|
],
|
||||||
"codeowners": ["@JeffLIrion"]
|
"codeowners": ["@JeffLIrion"]
|
||||||
|
@@ -5,15 +5,18 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from adb_shell.auth.keygen import keygen
|
from adb_shell.auth.keygen import keygen
|
||||||
|
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
|
||||||
from adb_shell.exceptions import (
|
from adb_shell.exceptions import (
|
||||||
|
AdbTimeoutError,
|
||||||
InvalidChecksumError,
|
InvalidChecksumError,
|
||||||
InvalidCommandError,
|
InvalidCommandError,
|
||||||
InvalidResponseError,
|
InvalidResponseError,
|
||||||
TcpTimeoutException,
|
TcpTimeoutException,
|
||||||
)
|
)
|
||||||
from androidtv import ha_state_detection_rules_validator, setup
|
from androidtv import ha_state_detection_rules_validator
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
from androidtv.exceptions import LockNotAcquiredException
|
from androidtv.exceptions import LockNotAcquiredException
|
||||||
|
from androidtv.setup_async import setup
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
||||||
@@ -44,7 +47,7 @@ from homeassistant.const import (
|
|||||||
STATE_STANDBY,
|
STATE_STANDBY,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.storage import STORAGE_DIR
|
from homeassistant.helpers.storage import STORAGE_DIR
|
||||||
|
|
||||||
ANDROIDTV_DOMAIN = "androidtv"
|
ANDROIDTV_DOMAIN = "androidtv"
|
||||||
@@ -103,6 +106,7 @@ DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
|
|||||||
|
|
||||||
SERVICE_ADB_COMMAND = "adb_command"
|
SERVICE_ADB_COMMAND = "adb_command"
|
||||||
SERVICE_DOWNLOAD = "download"
|
SERVICE_DOWNLOAD = "download"
|
||||||
|
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||||
SERVICE_UPLOAD = "upload"
|
SERVICE_UPLOAD = "upload"
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema(
|
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema(
|
||||||
@@ -161,7 +165,30 @@ ANDROIDTV_STATES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_androidtv(hass, config):
|
||||||
|
"""Generate an ADB key (if needed) and load it."""
|
||||||
|
adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey"))
|
||||||
|
if CONF_ADB_SERVER_IP not in config:
|
||||||
|
# Use "adb_shell" (Python ADB implementation)
|
||||||
|
if not os.path.isfile(adbkey):
|
||||||
|
# Generate ADB key files
|
||||||
|
keygen(adbkey)
|
||||||
|
|
||||||
|
# Load the ADB key
|
||||||
|
with open(adbkey) as priv_key:
|
||||||
|
priv = priv_key.read()
|
||||||
|
signer = PythonRSASigner("", priv)
|
||||||
|
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use "pure-python-adb" (communicate with ADB server)
|
||||||
|
signer = None
|
||||||
|
adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
|
||||||
|
|
||||||
|
return adbkey, signer, adb_log
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Android TV / Fire TV platform."""
|
"""Set up the Android TV / Fire TV platform."""
|
||||||
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
|
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
|
||||||
|
|
||||||
@@ -171,51 +198,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
_LOGGER.warning("Platform already setup on %s, skipping", address)
|
_LOGGER.warning("Platform already setup on %s, skipping", address)
|
||||||
return
|
return
|
||||||
|
|
||||||
if CONF_ADB_SERVER_IP not in config:
|
adbkey, signer, adb_log = await hass.async_add_executor_job(
|
||||||
# Use "adb_shell" (Python ADB implementation)
|
setup_androidtv, hass, config
|
||||||
if CONF_ADBKEY not in config:
|
)
|
||||||
# Generate ADB key files (if they don't exist)
|
|
||||||
adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey")
|
|
||||||
if not os.path.isfile(adbkey):
|
|
||||||
keygen(adbkey)
|
|
||||||
|
|
||||||
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
aftv = await setup(
|
||||||
|
config[CONF_HOST],
|
||||||
aftv = setup(
|
config[CONF_PORT],
|
||||||
config[CONF_HOST],
|
adbkey,
|
||||||
config[CONF_PORT],
|
config.get(CONF_ADB_SERVER_IP, ""),
|
||||||
adbkey,
|
config[CONF_ADB_SERVER_PORT],
|
||||||
device_class=config[CONF_DEVICE_CLASS],
|
config[CONF_STATE_DETECTION_RULES],
|
||||||
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
|
config[CONF_DEVICE_CLASS],
|
||||||
auth_timeout_s=10.0,
|
10.0,
|
||||||
)
|
signer,
|
||||||
|
)
|
||||||
else:
|
|
||||||
adb_log = (
|
|
||||||
f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
aftv = setup(
|
|
||||||
config[CONF_HOST],
|
|
||||||
config[CONF_PORT],
|
|
||||||
config[CONF_ADBKEY],
|
|
||||||
device_class=config[CONF_DEVICE_CLASS],
|
|
||||||
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
|
|
||||||
auth_timeout_s=10.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Use "pure-python-adb" (communicate with ADB server)
|
|
||||||
adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
|
|
||||||
|
|
||||||
aftv = setup(
|
|
||||||
config[CONF_HOST],
|
|
||||||
config[CONF_PORT],
|
|
||||||
adb_server_ip=config[CONF_ADB_SERVER_IP],
|
|
||||||
adb_server_port=config[CONF_ADB_SERVER_PORT],
|
|
||||||
device_class=config[CONF_DEVICE_CLASS],
|
|
||||||
state_detection_rules=config[CONF_STATE_DETECTION_RULES],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not aftv.available:
|
if not aftv.available:
|
||||||
# Determine the name that will be used for the device in the log
|
# Determine the name that will be used for the device in the log
|
||||||
@@ -251,14 +248,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
device = FireTVDevice(*device_args)
|
device = FireTVDevice(*device_args)
|
||||||
device_name = config.get(CONF_NAME, "Fire TV")
|
device_name = config.get(CONF_NAME, "Fire TV")
|
||||||
|
|
||||||
add_entities([device])
|
async_add_entities([device])
|
||||||
_LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log)
|
_LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log)
|
||||||
hass.data[ANDROIDTV_DOMAIN][address] = device
|
hass.data[ANDROIDTV_DOMAIN][address] = device
|
||||||
|
|
||||||
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
|
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
|
||||||
return
|
return
|
||||||
|
|
||||||
def service_adb_command(service):
|
platform = entity_platform.current_platform.get()
|
||||||
|
|
||||||
|
async def service_adb_command(service):
|
||||||
"""Dispatch service calls to target entities."""
|
"""Dispatch service calls to target entities."""
|
||||||
cmd = service.data[ATTR_COMMAND]
|
cmd = service.data[ATTR_COMMAND]
|
||||||
entity_id = service.data[ATTR_ENTITY_ID]
|
entity_id = service.data[ATTR_ENTITY_ID]
|
||||||
@@ -269,7 +268,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for target_device in target_devices:
|
for target_device in target_devices:
|
||||||
output = target_device.adb_command(cmd)
|
output = await target_device.adb_command(cmd)
|
||||||
|
|
||||||
# log the output, if there is any
|
# log the output, if there is any
|
||||||
if output:
|
if output:
|
||||||
@@ -280,14 +279,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
output,
|
output,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
ANDROIDTV_DOMAIN,
|
ANDROIDTV_DOMAIN,
|
||||||
SERVICE_ADB_COMMAND,
|
SERVICE_ADB_COMMAND,
|
||||||
service_adb_command,
|
service_adb_command,
|
||||||
schema=SERVICE_ADB_COMMAND_SCHEMA,
|
schema=SERVICE_ADB_COMMAND_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
def service_download(service):
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def service_download(service):
|
||||||
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
||||||
local_path = service.data[ATTR_LOCAL_PATH]
|
local_path = service.data[ATTR_LOCAL_PATH]
|
||||||
if not hass.config.is_allowed_path(local_path):
|
if not hass.config.is_allowed_path(local_path):
|
||||||
@@ -302,16 +305,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
if dev.entity_id in entity_id
|
if dev.entity_id in entity_id
|
||||||
][0]
|
][0]
|
||||||
|
|
||||||
target_device.adb_pull(local_path, device_path)
|
await target_device.adb_pull(local_path, device_path)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
ANDROIDTV_DOMAIN,
|
ANDROIDTV_DOMAIN,
|
||||||
SERVICE_DOWNLOAD,
|
SERVICE_DOWNLOAD,
|
||||||
service_download,
|
service_download,
|
||||||
schema=SERVICE_DOWNLOAD_SCHEMA,
|
schema=SERVICE_DOWNLOAD_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
def service_upload(service):
|
async def service_upload(service):
|
||||||
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
||||||
local_path = service.data[ATTR_LOCAL_PATH]
|
local_path = service.data[ATTR_LOCAL_PATH]
|
||||||
if not hass.config.is_allowed_path(local_path):
|
if not hass.config.is_allowed_path(local_path):
|
||||||
@@ -327,9 +330,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for target_device in target_devices:
|
for target_device in target_devices:
|
||||||
target_device.adb_push(local_path, device_path)
|
await target_device.adb_push(local_path, device_path)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA
|
ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -345,13 +348,13 @@ def adb_decorator(override_available=False):
|
|||||||
"""Wrap the provided ADB method and catch exceptions."""
|
"""Wrap the provided ADB method and catch exceptions."""
|
||||||
|
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def _adb_exception_catcher(self, *args, **kwargs):
|
async def _adb_exception_catcher(self, *args, **kwargs):
|
||||||
"""Call an ADB-related method and catch exceptions."""
|
"""Call an ADB-related method and catch exceptions."""
|
||||||
if not self.available and not override_available:
|
if not self.available and not override_available:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
except LockNotAcquiredException:
|
except LockNotAcquiredException:
|
||||||
# If the ADB lock could not be acquired, skip this command
|
# If the ADB lock could not be acquired, skip this command
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
@@ -364,7 +367,7 @@ def adb_decorator(override_available=False):
|
|||||||
"establishing attempt in the next update. Error: %s",
|
"establishing attempt in the next update. Error: %s",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
self.aftv.adb_close()
|
await self.aftv.adb_close()
|
||||||
self._available = False # pylint: disable=protected-access
|
self._available = False # pylint: disable=protected-access
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -411,6 +414,7 @@ class ADBDevice(MediaPlayerEntity):
|
|||||||
if not self.aftv.adb_server_ip:
|
if not self.aftv.adb_server_ip:
|
||||||
# Using "adb_shell" (Python ADB implementation)
|
# Using "adb_shell" (Python ADB implementation)
|
||||||
self.exceptions = (
|
self.exceptions = (
|
||||||
|
AdbTimeoutError,
|
||||||
AttributeError,
|
AttributeError,
|
||||||
BrokenPipeError,
|
BrokenPipeError,
|
||||||
ConnectionResetError,
|
ConnectionResetError,
|
||||||
@@ -487,64 +491,60 @@ class ADBDevice(MediaPlayerEntity):
|
|||||||
"""Return the device unique id."""
|
"""Return the device unique id."""
|
||||||
return self._unique_id
|
return self._unique_id
|
||||||
|
|
||||||
|
@adb_decorator()
|
||||||
async def async_get_media_image(self):
|
async def async_get_media_image(self):
|
||||||
"""Fetch current playing image."""
|
"""Fetch current playing image."""
|
||||||
if not self._screencap or self.state in [STATE_OFF, None] or not self.available:
|
if not self._screencap or self.state in [STATE_OFF, None] or not self.available:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
media_data = await self.hass.async_add_executor_job(self.get_raw_media_data)
|
media_data = await self.aftv.adb_screencap()
|
||||||
if media_data:
|
if media_data:
|
||||||
return media_data, "image/png"
|
return media_data, "image/png"
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def get_raw_media_data(self):
|
async def async_media_play(self):
|
||||||
"""Raw image data."""
|
|
||||||
return self.aftv.adb_screencap()
|
|
||||||
|
|
||||||
@adb_decorator()
|
|
||||||
def media_play(self):
|
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
self.aftv.media_play()
|
await self.aftv.media_play()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_pause(self):
|
async def async_media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
self.aftv.media_pause()
|
await self.aftv.media_pause()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_play_pause(self):
|
async def async_media_play_pause(self):
|
||||||
"""Send play/pause command."""
|
"""Send play/pause command."""
|
||||||
self.aftv.media_play_pause()
|
await self.aftv.media_play_pause()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def turn_on(self):
|
async def async_turn_on(self):
|
||||||
"""Turn on the device."""
|
"""Turn on the device."""
|
||||||
if self.turn_on_command:
|
if self.turn_on_command:
|
||||||
self.aftv.adb_shell(self.turn_on_command)
|
await self.aftv.adb_shell(self.turn_on_command)
|
||||||
else:
|
else:
|
||||||
self.aftv.turn_on()
|
await self.aftv.turn_on()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def turn_off(self):
|
async def async_turn_off(self):
|
||||||
"""Turn off the device."""
|
"""Turn off the device."""
|
||||||
if self.turn_off_command:
|
if self.turn_off_command:
|
||||||
self.aftv.adb_shell(self.turn_off_command)
|
await self.aftv.adb_shell(self.turn_off_command)
|
||||||
else:
|
else:
|
||||||
self.aftv.turn_off()
|
await self.aftv.turn_off()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_previous_track(self):
|
async def async_media_previous_track(self):
|
||||||
"""Send previous track command (results in rewind)."""
|
"""Send previous track command (results in rewind)."""
|
||||||
self.aftv.media_previous_track()
|
await self.aftv.media_previous_track()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_next_track(self):
|
async def async_media_next_track(self):
|
||||||
"""Send next track command (results in fast-forward)."""
|
"""Send next track command (results in fast-forward)."""
|
||||||
self.aftv.media_next_track()
|
await self.aftv.media_next_track()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def select_source(self, source):
|
async def async_select_source(self, source):
|
||||||
"""Select input source.
|
"""Select input source.
|
||||||
|
|
||||||
If the source starts with a '!', then it will close the app instead of
|
If the source starts with a '!', then it will close the app instead of
|
||||||
@@ -552,50 +552,58 @@ class ADBDevice(MediaPlayerEntity):
|
|||||||
"""
|
"""
|
||||||
if isinstance(source, str):
|
if isinstance(source, str):
|
||||||
if not source.startswith("!"):
|
if not source.startswith("!"):
|
||||||
self.aftv.launch_app(self._app_name_to_id.get(source, source))
|
await self.aftv.launch_app(self._app_name_to_id.get(source, source))
|
||||||
else:
|
else:
|
||||||
source_ = source[1:].lstrip()
|
source_ = source[1:].lstrip()
|
||||||
self.aftv.stop_app(self._app_name_to_id.get(source_, source_))
|
await self.aftv.stop_app(self._app_name_to_id.get(source_, source_))
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def adb_command(self, cmd):
|
async def adb_command(self, cmd):
|
||||||
"""Send an ADB command to an Android TV / Fire TV device."""
|
"""Send an ADB command to an Android TV / Fire TV device."""
|
||||||
key = self._keys.get(cmd)
|
key = self._keys.get(cmd)
|
||||||
if key:
|
if key:
|
||||||
self.aftv.adb_shell(f"input keyevent {key}")
|
await self.aftv.adb_shell(f"input keyevent {key}")
|
||||||
self._adb_response = None
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "GET_PROPERTIES":
|
if cmd == "GET_PROPERTIES":
|
||||||
self._adb_response = str(self.aftv.get_properties_dict())
|
self._adb_response = str(await self.aftv.get_properties_dict())
|
||||||
self.schedule_update_ha_state()
|
self.async_write_ha_state()
|
||||||
return self._adb_response
|
return self._adb_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.aftv.adb_shell(cmd)
|
response = await self.aftv.adb_shell(cmd)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
self._adb_response = None
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(response, str) and response.strip():
|
if isinstance(response, str) and response.strip():
|
||||||
self._adb_response = response.strip()
|
self._adb_response = response.strip()
|
||||||
else:
|
self.async_write_ha_state()
|
||||||
self._adb_response = None
|
|
||||||
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
return self._adb_response
|
return self._adb_response
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def adb_pull(self, local_path, device_path):
|
async def learn_sendevent(self):
|
||||||
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
"""Translate a key press on a remote to ADB 'sendevent' commands."""
|
||||||
self.aftv.adb_pull(local_path, device_path)
|
output = await self.aftv.learn_sendevent()
|
||||||
|
if output:
|
||||||
|
self._adb_response = output
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'"
|
||||||
|
self.hass.components.persistent_notification.async_create(
|
||||||
|
msg, title="Android TV",
|
||||||
|
)
|
||||||
|
_LOGGER.info("%s", msg)
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def adb_push(self, local_path, device_path):
|
async def adb_pull(self, local_path, device_path):
|
||||||
|
"""Download a file from your Android TV / Fire TV device to your Home Assistant instance."""
|
||||||
|
await self.aftv.adb_pull(local_path, device_path)
|
||||||
|
|
||||||
|
@adb_decorator()
|
||||||
|
async def adb_push(self, local_path, device_path):
|
||||||
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
"""Upload a file from your Home Assistant instance to an Android TV / Fire TV device."""
|
||||||
self.aftv.adb_push(local_path, device_path)
|
await self.aftv.adb_push(local_path, device_path)
|
||||||
|
|
||||||
|
|
||||||
class AndroidTVDevice(ADBDevice):
|
class AndroidTVDevice(ADBDevice):
|
||||||
@@ -628,17 +636,12 @@ class AndroidTVDevice(ADBDevice):
|
|||||||
self._volume_level = None
|
self._volume_level = None
|
||||||
|
|
||||||
@adb_decorator(override_available=True)
|
@adb_decorator(override_available=True)
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update the device state and, if necessary, re-connect."""
|
"""Update the device state and, if necessary, re-connect."""
|
||||||
# Check if device is disconnected.
|
# Check if device is disconnected.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
# Try to connect
|
# Try to connect
|
||||||
self._available = self.aftv.adb_connect(always_log_errors=False)
|
self._available = await self.aftv.adb_connect(always_log_errors=False)
|
||||||
|
|
||||||
# To be safe, wait until the next update to run ADB commands if
|
|
||||||
# using the Python ADB implementation.
|
|
||||||
if not self.aftv.adb_server_ip:
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the ADB connection is not intact, don't update.
|
# If the ADB connection is not intact, don't update.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
@@ -652,7 +655,7 @@ class AndroidTVDevice(ADBDevice):
|
|||||||
_,
|
_,
|
||||||
self._is_volume_muted,
|
self._is_volume_muted,
|
||||||
self._volume_level,
|
self._volume_level,
|
||||||
) = self.aftv.update(self._get_sources)
|
) = await self.aftv.update(self._get_sources)
|
||||||
|
|
||||||
self._state = ANDROIDTV_STATES.get(state)
|
self._state = ANDROIDTV_STATES.get(state)
|
||||||
if self._state is None:
|
if self._state is None:
|
||||||
@@ -685,53 +688,50 @@ class AndroidTVDevice(ADBDevice):
|
|||||||
return self._volume_level
|
return self._volume_level
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_stop(self):
|
async def async_media_stop(self):
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
self.aftv.media_stop()
|
await self.aftv.media_stop()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def mute_volume(self, mute):
|
async def async_mute_volume(self, mute):
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
self.aftv.mute_volume()
|
await self.aftv.mute_volume()
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def set_volume_level(self, volume):
|
async def async_set_volume_level(self, volume):
|
||||||
"""Set the volume level."""
|
"""Set the volume level."""
|
||||||
self.aftv.set_volume_level(volume)
|
await self.aftv.set_volume_level(volume)
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def volume_down(self):
|
async def async_volume_down(self):
|
||||||
"""Send volume down command."""
|
"""Send volume down command."""
|
||||||
self._volume_level = self.aftv.volume_down(self._volume_level)
|
self._volume_level = await self.aftv.volume_down(self._volume_level)
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def volume_up(self):
|
async def async_volume_up(self):
|
||||||
"""Send volume up command."""
|
"""Send volume up command."""
|
||||||
self._volume_level = self.aftv.volume_up(self._volume_level)
|
self._volume_level = await self.aftv.volume_up(self._volume_level)
|
||||||
|
|
||||||
|
|
||||||
class FireTVDevice(ADBDevice):
|
class FireTVDevice(ADBDevice):
|
||||||
"""Representation of a Fire TV device."""
|
"""Representation of a Fire TV device."""
|
||||||
|
|
||||||
@adb_decorator(override_available=True)
|
@adb_decorator(override_available=True)
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update the device state and, if necessary, re-connect."""
|
"""Update the device state and, if necessary, re-connect."""
|
||||||
# Check if device is disconnected.
|
# Check if device is disconnected.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
# Try to connect
|
# Try to connect
|
||||||
self._available = self.aftv.adb_connect(always_log_errors=False)
|
self._available = await self.aftv.adb_connect(always_log_errors=False)
|
||||||
|
|
||||||
# To be safe, wait until the next update to run ADB commands if
|
|
||||||
# using the Python ADB implementation.
|
|
||||||
if not self.aftv.adb_server_ip:
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the ADB connection is not intact, don't update.
|
# If the ADB connection is not intact, don't update.
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the `state`, `current_app`, and `running_apps`.
|
# Get the `state`, `current_app`, and `running_apps`.
|
||||||
state, self._current_app, running_apps = self.aftv.update(self._get_sources)
|
state, self._current_app, running_apps = await self.aftv.update(
|
||||||
|
self._get_sources
|
||||||
|
)
|
||||||
|
|
||||||
self._state = ANDROIDTV_STATES.get(state)
|
self._state = ANDROIDTV_STATES.get(state)
|
||||||
if self._state is None:
|
if self._state is None:
|
||||||
@@ -754,6 +754,6 @@ class FireTVDevice(ADBDevice):
|
|||||||
return SUPPORT_FIRETV
|
return SUPPORT_FIRETV
|
||||||
|
|
||||||
@adb_decorator()
|
@adb_decorator()
|
||||||
def media_stop(self):
|
async def async_media_stop(self):
|
||||||
"""Send stop (back) command."""
|
"""Send stop (back) command."""
|
||||||
self.aftv.back()
|
await self.aftv.back()
|
||||||
|
@@ -33,3 +33,9 @@ upload:
|
|||||||
local_path:
|
local_path:
|
||||||
description: The filepath on your Home Assistant instance.
|
description: The filepath on your Home Assistant instance.
|
||||||
example: "/config/www/example.txt"
|
example: "/config/www/example.txt"
|
||||||
|
learn_sendevent:
|
||||||
|
description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of Android TV / Fire TV entities.
|
||||||
|
example: "media_player.android_tv_living_room"
|
||||||
|
@@ -2,6 +2,6 @@
|
|||||||
"domain": "apache_kafka",
|
"domain": "apache_kafka",
|
||||||
"name": "Apache Kafka",
|
"name": "Apache Kafka",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apache_kafka",
|
"documentation": "https://www.home-assistant.io/integrations/apache_kafka",
|
||||||
"requirements": ["aiokafka==0.5.1"],
|
"requirements": ["aiokafka==0.6.0"],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ def setup(hass, config):
|
|||||||
try:
|
try:
|
||||||
apcups_data.update(no_throttle=True)
|
apcups_data.update(no_throttle=True)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
_LOGGER.exception("Failure while testing APCUPSd status retrieval")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@@ -36,13 +36,13 @@ SENSOR_TYPES = {
|
|||||||
"battv": ["Battery Voltage", VOLT, "mdi:flash"],
|
"battv": ["Battery Voltage", VOLT, "mdi:flash"],
|
||||||
"bcharge": ["Battery", UNIT_PERCENTAGE, "mdi:battery"],
|
"bcharge": ["Battery", UNIT_PERCENTAGE, "mdi:battery"],
|
||||||
"cable": ["Cable Type", "", "mdi:ethernet-cable"],
|
"cable": ["Cable Type", "", "mdi:ethernet-cable"],
|
||||||
"cumonbatt": ["Total Time on Battery", "", "mdi:timer"],
|
"cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline"],
|
||||||
"date": ["Status Date", "", "mdi:calendar-clock"],
|
"date": ["Status Date", "", "mdi:calendar-clock"],
|
||||||
"dipsw": ["Dip Switch Settings", "", "mdi:information-outline"],
|
"dipsw": ["Dip Switch Settings", "", "mdi:information-outline"],
|
||||||
"dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"],
|
"dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"],
|
||||||
"driver": ["Driver", "", "mdi:information-outline"],
|
"driver": ["Driver", "", "mdi:information-outline"],
|
||||||
"dshutd": ["Shutdown Delay", "", "mdi:timer"],
|
"dshutd": ["Shutdown Delay", "", "mdi:timer-outline"],
|
||||||
"dwake": ["Wake Delay", "", "mdi:timer"],
|
"dwake": ["Wake Delay", "", "mdi:timer-outline"],
|
||||||
"endapc": ["Date and Time", "", "mdi:calendar-clock"],
|
"endapc": ["Date and Time", "", "mdi:calendar-clock"],
|
||||||
"extbatts": ["External Batteries", "", "mdi:information-outline"],
|
"extbatts": ["External Batteries", "", "mdi:information-outline"],
|
||||||
"firmware": ["Firmware Version", "", "mdi:information-outline"],
|
"firmware": ["Firmware Version", "", "mdi:information-outline"],
|
||||||
@@ -60,10 +60,10 @@ SENSOR_TYPES = {
|
|||||||
"mandate": ["Manufacture Date", "", "mdi:calendar"],
|
"mandate": ["Manufacture Date", "", "mdi:calendar"],
|
||||||
"masterupd": ["Master Update", "", "mdi:information-outline"],
|
"masterupd": ["Master Update", "", "mdi:information-outline"],
|
||||||
"maxlinev": ["Input Voltage High", VOLT, "mdi:flash"],
|
"maxlinev": ["Input Voltage High", VOLT, "mdi:flash"],
|
||||||
"maxtime": ["Battery Timeout", "", "mdi:timer-off"],
|
"maxtime": ["Battery Timeout", "", "mdi:timer-off-outline"],
|
||||||
"mbattchg": ["Battery Shutdown", UNIT_PERCENTAGE, "mdi:battery-alert"],
|
"mbattchg": ["Battery Shutdown", UNIT_PERCENTAGE, "mdi:battery-alert"],
|
||||||
"minlinev": ["Input Voltage Low", VOLT, "mdi:flash"],
|
"minlinev": ["Input Voltage Low", VOLT, "mdi:flash"],
|
||||||
"mintimel": ["Shutdown Time", "", "mdi:timer"],
|
"mintimel": ["Shutdown Time", "", "mdi:timer-outline"],
|
||||||
"model": ["Model", "", "mdi:information-outline"],
|
"model": ["Model", "", "mdi:information-outline"],
|
||||||
"nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash"],
|
"nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash"],
|
||||||
"nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"],
|
"nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"],
|
||||||
@@ -85,7 +85,7 @@ SENSOR_TYPES = {
|
|||||||
"status": ["Status", "", "mdi:information-outline"],
|
"status": ["Status", "", "mdi:information-outline"],
|
||||||
"stesti": ["Self Test Interval", "", "mdi:information-outline"],
|
"stesti": ["Self Test Interval", "", "mdi:information-outline"],
|
||||||
"timeleft": ["Time Left", "", "mdi:clock-alert"],
|
"timeleft": ["Time Left", "", "mdi:clock-alert"],
|
||||||
"tonbatt": ["Time on Battery", "", "mdi:timer"],
|
"tonbatt": ["Time on Battery", "", "mdi:timer-outline"],
|
||||||
"upsmode": ["Mode", "", "mdi:information-outline"],
|
"upsmode": ["Mode", "", "mdi:information-outline"],
|
||||||
"upsname": ["Name", "", "mdi:information-outline"],
|
"upsname": ["Name", "", "mdi:information-outline"],
|
||||||
"version": ["Daemon Info", "", "mdi:information-outline"],
|
"version": ["Daemon Info", "", "mdi:information-outline"],
|
||||||
|
@@ -97,7 +97,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect)
|
||||||
|
|
||||||
if not aprs_listener.start_event.wait(timeout):
|
if not aprs_listener.start_event.wait(timeout):
|
||||||
_LOGGER.error("Timeout waiting for APRS to connect.")
|
_LOGGER.error("Timeout waiting for APRS to connect")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not aprs_listener.start_success:
|
if not aprs_listener.start_success:
|
||||||
@@ -141,7 +141,7 @@ class AprsListenerThread(threading.Thread):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Opening connection to %s with callsign %s.", self.host, self.callsign
|
"Opening connection to %s with callsign %s", self.host, self.callsign
|
||||||
)
|
)
|
||||||
self.ais.connect()
|
self.ais.connect()
|
||||||
self.start_complete(
|
self.start_complete(
|
||||||
@@ -152,7 +152,7 @@ class AprsListenerThread(threading.Thread):
|
|||||||
self.start_complete(False, str(err))
|
self.start_complete(False, str(err))
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Closing connection to %s with callsign %s.", self.host, self.callsign
|
"Closing connection to %s with callsign %s", self.host, self.callsign
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
@@ -5,27 +5,15 @@ import logging
|
|||||||
from arcam.fmj import ConnectionFailed
|
from arcam.fmj import ConnectionFailed
|
||||||
from arcam.fmj.client import Client
|
from arcam.fmj.client import Client
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
|
||||||
CONF_HOST,
|
|
||||||
CONF_NAME,
|
|
||||||
CONF_PORT,
|
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_ZONE,
|
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DEFAULT_NAME,
|
|
||||||
DEFAULT_PORT,
|
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_DATA_CONFIG,
|
|
||||||
DOMAIN_DATA_ENTRIES,
|
DOMAIN_DATA_ENTRIES,
|
||||||
DOMAIN_DATA_TASKS,
|
DOMAIN_DATA_TASKS,
|
||||||
SIGNAL_CLIENT_DATA,
|
SIGNAL_CLIENT_DATA,
|
||||||
@@ -35,44 +23,7 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")
|
||||||
def _optional_zone(value):
|
|
||||||
if value:
|
|
||||||
return ZONE_SCHEMA(value)
|
|
||||||
return ZONE_SCHEMA({})
|
|
||||||
|
|
||||||
|
|
||||||
def _zone_name_validator(config):
|
|
||||||
for zone, zone_config in config[CONF_ZONE].items():
|
|
||||||
if CONF_NAME not in zone_config:
|
|
||||||
zone_config[
|
|
||||||
CONF_NAME
|
|
||||||
] = f"{DEFAULT_NAME} ({config[CONF_HOST]}:{config[CONF_PORT]}) - {zone}"
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
ZONE_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
|
||||||
vol.Optional(SERVICE_TURN_ON): cv.SERVICE_SCHEMA,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema(
|
|
||||||
vol.All(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_HOST): cv.string,
|
|
||||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
|
|
||||||
vol.Optional(CONF_ZONE, default={1: _optional_zone(None)}): {
|
|
||||||
vol.In([1, 2]): _optional_zone
|
|
||||||
},
|
|
||||||
vol.Optional(
|
|
||||||
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
|
||||||
): cv.positive_int,
|
|
||||||
},
|
|
||||||
_zone_name_validator,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _await_cancel(task):
|
async def _await_cancel(task):
|
||||||
@@ -83,27 +34,10 @@ async def _await_cancel(task):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
|
||||||
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
"""Set up the component."""
|
"""Set up the component."""
|
||||||
hass.data[DOMAIN_DATA_ENTRIES] = {}
|
hass.data[DOMAIN_DATA_ENTRIES] = {}
|
||||||
hass.data[DOMAIN_DATA_TASKS] = {}
|
hass.data[DOMAIN_DATA_TASKS] = {}
|
||||||
hass.data[DOMAIN_DATA_CONFIG] = {}
|
|
||||||
|
|
||||||
for device in config[DOMAIN]:
|
|
||||||
hass.data[DOMAIN_DATA_CONFIG][(device[CONF_HOST], device[CONF_PORT])] = device
|
|
||||||
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
|
||||||
data={CONF_HOST: device[CONF_HOST], CONF_PORT: device[CONF_PORT]},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _stop(_):
|
async def _stop(_):
|
||||||
asyncio.gather(
|
asyncio.gather(
|
||||||
@@ -116,21 +50,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry):
|
||||||
"""Set up an access point from a config entry."""
|
"""Set up config entry."""
|
||||||
|
entries = hass.data[DOMAIN_DATA_ENTRIES]
|
||||||
|
tasks = hass.data[DOMAIN_DATA_TASKS]
|
||||||
|
|
||||||
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||||
|
entries[entry.entry_id] = client
|
||||||
config = hass.data[DOMAIN_DATA_CONFIG].get(
|
|
||||||
(entry.data[CONF_HOST], entry.data[CONF_PORT]),
|
|
||||||
DEVICE_SCHEMA(
|
|
||||||
{CONF_HOST: entry.data[CONF_HOST], CONF_PORT: entry.data[CONF_PORT]}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
tasks = hass.data.setdefault(DOMAIN_DATA_TASKS, {})
|
|
||||||
|
|
||||||
hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
|
|
||||||
"client": client,
|
|
||||||
"config": config,
|
|
||||||
}
|
|
||||||
|
|
||||||
task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL))
|
task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL))
|
||||||
tasks[entry.entry_id] = task
|
tasks[entry.entry_id] = task
|
||||||
|
@@ -1,27 +1,102 @@
|
|||||||
"""Config flow to configure the Arcam FMJ component."""
|
"""Config flow to configure the Arcam FMJ component."""
|
||||||
from operator import itemgetter
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from arcam.fmj.client import Client, ConnectionFailed
|
||||||
|
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_UDN
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES
|
||||||
|
|
||||||
_GETKEY = itemgetter(CONF_HOST, CONF_PORT)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entry_client(hass, entry):
|
||||||
|
"""Retrieve client associated with a config entry."""
|
||||||
|
return hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
|
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
|
||||||
"""Handle a SimpliSafe config flow."""
|
"""Handle config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
async def async_step_import(self, import_config):
|
async def _async_set_unique_id_and_update(self, host, port, uuid):
|
||||||
"""Import a config entry from configuration.yaml."""
|
await self.async_set_unique_id(uuid)
|
||||||
entries = self.hass.config_entries.async_entries(DOMAIN)
|
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
|
||||||
import_key = _GETKEY(import_config)
|
|
||||||
for entry in entries:
|
|
||||||
if _GETKEY(entry.data) == import_key:
|
|
||||||
return self.async_abort(reason="already_setup")
|
|
||||||
|
|
||||||
return self.async_create_entry(title="Arcam FMJ", data=import_config)
|
async def _async_check_and_create(self, host, port):
|
||||||
|
client = Client(host, port)
|
||||||
|
try:
|
||||||
|
await client.start()
|
||||||
|
except ConnectionFailed:
|
||||||
|
return self.async_abort(reason="unable_to_connect")
|
||||||
|
finally:
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a discovered device."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
uuid = await get_uniqueid_from_host(
|
||||||
|
async_get_clientsession(self.hass), user_input[CONF_HOST]
|
||||||
|
)
|
||||||
|
if uuid:
|
||||||
|
await self._async_set_unique_id_and_update(
|
||||||
|
user_input[CONF_HOST], user_input[CONF_PORT], uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._async_check_and_create(
|
||||||
|
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||||
|
)
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=vol.Schema(fields), errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_confirm(self, user_input=None):
|
||||||
|
"""Handle user-confirmation of discovered node."""
|
||||||
|
context = self.context # pylint: disable=no-member
|
||||||
|
placeholders = {
|
||||||
|
"host": context[CONF_HOST],
|
||||||
|
}
|
||||||
|
context["title_placeholders"] = placeholders
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
return await self._async_check_and_create(
|
||||||
|
context[CONF_HOST], context[CONF_PORT]
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm", description_placeholders=placeholders
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_ssdp(self, discovery_info):
|
||||||
|
"""Handle a discovered device."""
|
||||||
|
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
|
||||||
|
port = DEFAULT_PORT
|
||||||
|
uuid = get_uniqueid_from_udn(discovery_info[ATTR_UPNP_UDN])
|
||||||
|
|
||||||
|
await self._async_set_unique_id_and_update(host, port, uuid)
|
||||||
|
|
||||||
|
context = self.context # pylint: disable=no-member
|
||||||
|
context[CONF_HOST] = host
|
||||||
|
context[CONF_PORT] = DEFAULT_PORT
|
||||||
|
return await self.async_step_confirm()
|
||||||
|
@@ -13,4 +13,3 @@ DEFAULT_SCAN_INTERVAL = 5
|
|||||||
|
|
||||||
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"
|
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"
|
||||||
DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks"
|
DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks"
|
||||||
DOMAIN_DATA_CONFIG = f"{DOMAIN}.config"
|
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
{
|
{
|
||||||
"domain": "arcam_fmj",
|
"domain": "arcam_fmj",
|
||||||
"name": "Arcam FMJ Receivers",
|
"name": "Arcam FMJ Receivers",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||||
"requirements": ["arcam-fmj==0.4.6"],
|
"requirements": ["arcam-fmj==0.5.1"],
|
||||||
|
"ssdp": [
|
||||||
|
{
|
||||||
|
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||||
|
"manufacturer": "ARCAM"
|
||||||
|
}
|
||||||
|
],
|
||||||
"codeowners": ["@elupus"]
|
"codeowners": ["@elupus"]
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
"""Arcam media player."""
|
"""Arcam media player."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes
|
from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes
|
||||||
from arcam.fmj.state import State
|
from arcam.fmj.state import State
|
||||||
@@ -17,21 +16,13 @@ from homeassistant.components.media_player.const import (
|
|||||||
SUPPORT_VOLUME_SET,
|
SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_VOLUME_STEP,
|
SUPPORT_VOLUME_STEP,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||||
ATTR_ENTITY_ID,
|
|
||||||
CONF_NAME,
|
|
||||||
CONF_ZONE,
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
STATE_OFF,
|
|
||||||
STATE_ON,
|
|
||||||
)
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.service import async_call_from_config
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
|
||||||
|
|
||||||
|
from .config_flow import get_entry_client
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DOMAIN_DATA_ENTRIES,
|
|
||||||
EVENT_TURN_ON,
|
EVENT_TURN_ON,
|
||||||
SIGNAL_CLIENT_DATA,
|
SIGNAL_CLIENT_DATA,
|
||||||
SIGNAL_CLIENT_STARTED,
|
SIGNAL_CLIENT_STARTED,
|
||||||
@@ -47,19 +38,17 @@ async def async_setup_entry(
|
|||||||
async_add_entities,
|
async_add_entities,
|
||||||
):
|
):
|
||||||
"""Set up the configuration entry."""
|
"""Set up the configuration entry."""
|
||||||
data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id]
|
|
||||||
client = data["client"]
|
client = get_entry_client(hass, config_entry)
|
||||||
config = data["config"]
|
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
ArcamFmj(
|
ArcamFmj(
|
||||||
|
config_entry.title,
|
||||||
State(client, zone),
|
State(client, zone),
|
||||||
config_entry.unique_id or config_entry.entry_id,
|
config_entry.unique_id or config_entry.entry_id,
|
||||||
zone_config[CONF_NAME],
|
|
||||||
zone_config.get(SERVICE_TURN_ON),
|
|
||||||
)
|
)
|
||||||
for zone, zone_config in config[CONF_ZONE].items()
|
for zone in [1, 2]
|
||||||
],
|
],
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
@@ -71,13 +60,13 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
"""Representation of a media device."""
|
"""Representation of a media device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, state: State, uuid: str, name: str, turn_on: Optional[ConfigType]
|
self, device_name, state: State, uuid: str,
|
||||||
):
|
):
|
||||||
"""Initialize device."""
|
"""Initialize device."""
|
||||||
self._state = state
|
self._state = state
|
||||||
|
self._device_name = device_name
|
||||||
|
self._name = f"{device_name} - Zone: {state.zn}"
|
||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
self._name = name
|
|
||||||
self._turn_on = turn_on
|
|
||||||
self._support = (
|
self._support = (
|
||||||
SUPPORT_SELECT_SOURCE
|
SUPPORT_SELECT_SOURCE
|
||||||
| SUPPORT_VOLUME_SET
|
| SUPPORT_VOLUME_SET
|
||||||
@@ -102,6 +91,11 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return self._state.zn == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return unique identifier if known."""
|
"""Return unique identifier if known."""
|
||||||
@@ -111,8 +105,12 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return {
|
return {
|
||||||
"identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)},
|
"name": self._device_name,
|
||||||
"model": "FMJ",
|
"identifiers": {
|
||||||
|
(DOMAIN, self._uuid),
|
||||||
|
(DOMAIN, self._state.client.host, self._state.client.port),
|
||||||
|
},
|
||||||
|
"model": "Arcam FMJ AVR",
|
||||||
"manufacturer": "Arcam",
|
"manufacturer": "Arcam",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,15 +227,6 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
if self._state.get_power() is not None:
|
if self._state.get_power() is not None:
|
||||||
_LOGGER.debug("Turning on device using connection")
|
_LOGGER.debug("Turning on device using connection")
|
||||||
await self._state.set_power(True)
|
await self._state.set_power(True)
|
||||||
elif self._turn_on:
|
|
||||||
_LOGGER.debug("Turning on device using service call")
|
|
||||||
await async_call_from_config(
|
|
||||||
self.hass,
|
|
||||||
self._turn_on,
|
|
||||||
variables=None,
|
|
||||||
blocking=True,
|
|
||||||
validate_config=False,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Firing event to turn on device")
|
_LOGGER.debug("Firing event to turn on device")
|
||||||
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
|
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
|
||||||
|
@@ -1,4 +1,25 @@
|
|||||||
{
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device was already setup.",
|
||||||
|
"already_in_progress": "Config flow for device is already in progress.",
|
||||||
|
"unable_to_connect": "Unable to connect to device."
|
||||||
|
},
|
||||||
|
"error": {},
|
||||||
|
"flow_title": "Arcam FMJ on {host}",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
|
},
|
||||||
|
"description": "Please enter the host name or IP address of device."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"turn_on": "{entity_name} was requested to turn on"
|
"turn_on": "{entity_name} was requested to turn on"
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Arcam FMJ"
|
|
||||||
}
|
|
13
homeassistant/components/arcam_fmj/translations/cs.json
Normal file
13
homeassistant/components/arcam_fmj/translations/cs.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Hostitel",
|
||||||
|
"port": "Port"
|
||||||
|
},
|
||||||
|
"description": "Zadejte n\u00e1zev hostitele nebo IP adresu za\u0159\u00edzen\u00ed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Arcam FMJ"
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Arcam FMJ"
|
|
||||||
}
|
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "El dispositivo ya est\u00e1 configurado.",
|
"already_configured": "El dispositivo ya est\u00e1 configurado.",
|
||||||
"already_in_progress": "La configuraci\u00f3n del flujo para el dispositivo ya est\u00e1 en marcha.",
|
"already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en marcha.",
|
||||||
"unable_to_connect": "No se puede conectar con el dispositivo."
|
"unable_to_connect": "No se puede conectar con el dispositivo."
|
||||||
},
|
},
|
||||||
"flow_title": "Arcam FMJ en {host}",
|
"flow_title": "Arcam FMJ en {host}",
|
||||||
|
@@ -1,4 +1,28 @@
|
|||||||
{
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "L'appareil \u00e9tait d\u00e9j\u00e0 configur\u00e9.",
|
||||||
|
"already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.",
|
||||||
|
"unable_to_connect": "Impossible de se connecter au p\u00e9riph\u00e9rique."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"one": "Vide",
|
||||||
|
"other": "Vide"
|
||||||
|
},
|
||||||
|
"flow_title": "Arcam FMJ sur {host}",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Voulez-vous ajouter Arcam FMJ sur ` {host} ` \u00e0 HomeAssistant ?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "H\u00f4te",
|
||||||
|
"port": "Port"
|
||||||
|
},
|
||||||
|
"description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP du p\u00e9riph\u00e9rique."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {nom_de_l'entit\u00e9} de s'allumer"
|
"turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {nom_de_l'entit\u00e9} de s'allumer"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user