mirror of
https://github.com/home-assistant/core.git
synced 2025-09-26 05:19:24 +00:00
Compare commits
896 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a79a0aa90 | ||
![]() |
ccc4f628f1 | ||
![]() |
90231c5e07 | ||
![]() |
ff5dd0cf42 | ||
![]() |
5d7f420821 | ||
![]() |
e4bb955498 | ||
![]() |
74d0e65958 | ||
![]() |
3cfbbdc720 | ||
![]() |
3d5c773670 | ||
![]() |
b5b0f56ae7 | ||
![]() |
c03d5f1a73 | ||
![]() |
5abe4dd1f7 | ||
![]() |
026dbffa77 | ||
![]() |
e74fc9836d | ||
![]() |
c7dfec702d | ||
![]() |
0f8f9db319 | ||
![]() |
366ad8202a | ||
![]() |
28bd7b6a4e | ||
![]() |
c04049d6f6 | ||
![]() |
ff79e437d2 | ||
![]() |
c8b495f224 | ||
![]() |
842c1a2274 | ||
![]() |
50b145cf05 | ||
![]() |
78a0d72a5c | ||
![]() |
97ca0d81e7 | ||
![]() |
02e8ee137f | ||
![]() |
2643bbc228 | ||
![]() |
c8d7e1346c | ||
![]() |
7dedf173ad | ||
![]() |
65593e36b1 | ||
![]() |
8996e330b8 | ||
![]() |
e85d434f4e | ||
![]() |
82d9488ec8 | ||
![]() |
4fc302b67a | ||
![]() |
9548345ed0 | ||
![]() |
d444ba397b | ||
![]() |
c884f9edbc | ||
![]() |
d0af73efe1 | ||
![]() |
5eb7268ae7 | ||
![]() |
60c2e5e2e2 | ||
![]() |
4e69b5b45f | ||
![]() |
1d784bdc05 | ||
![]() |
9181660497 | ||
![]() |
7fc8ff982b | ||
![]() |
155c75c54a | ||
![]() |
afade4e997 | ||
![]() |
53111f6426 | ||
![]() |
53a701b12c | ||
![]() |
a0e45cce79 | ||
![]() |
2b62ea1f0e | ||
![]() |
cc7b65a6c8 | ||
![]() |
3b6b421152 | ||
![]() |
fcb1783f56 | ||
![]() |
8041339052 | ||
![]() |
87d3680630 | ||
![]() |
bd7c0e87d5 | ||
![]() |
df920b4eda | ||
![]() |
cde3f670c2 | ||
![]() |
c80683bb15 | ||
![]() |
073327831f | ||
![]() |
312fceeaf6 | ||
![]() |
42d2f30ab8 | ||
![]() |
4844477d3a | ||
![]() |
ca8118138c | ||
![]() |
e51b5e801e | ||
![]() |
9ccb85d959 | ||
![]() |
cea857e18a | ||
![]() |
1afa136fc0 | ||
![]() |
7d33b0a259 | ||
![]() |
777e1ca832 | ||
![]() |
2e26f0bd2b | ||
![]() |
236debb455 | ||
![]() |
5f5c541f2f | ||
![]() |
f0f7dc4884 | ||
![]() |
18d27c997d | ||
![]() |
a44686389c | ||
![]() |
98ba015f06 | ||
![]() |
c1c2159dee | ||
![]() |
a30c37017b | ||
![]() |
195b034abc | ||
![]() |
c5239c6176 | ||
![]() |
5be695c49c | ||
![]() |
8652c84745 | ||
![]() |
36ed725ab4 | ||
![]() |
cf5a35a421 | ||
![]() |
8256d72f6d | ||
![]() |
25745e9e27 | ||
![]() |
3ce1049d21 | ||
![]() |
f3e542542a | ||
![]() |
07b635e7aa | ||
![]() |
c2e843cbc3 | ||
![]() |
7a5fca69af | ||
![]() |
3016d3a186 | ||
![]() |
a31e49c857 | ||
![]() |
2fbbcafaed | ||
![]() |
a2237ce5d4 | ||
![]() |
af7f61fec2 | ||
![]() |
26a66276cd | ||
![]() |
9944e675a5 | ||
![]() |
f9b9883aba | ||
![]() |
1431fd6fbd | ||
![]() |
b11171aaeb | ||
![]() |
0b7a901c81 | ||
![]() |
662e0dde80 | ||
![]() |
ab832cda71 | ||
![]() |
f90fe7e628 | ||
![]() |
32685f16bf | ||
![]() |
84cf76ba36 | ||
![]() |
c2f1c4b981 | ||
![]() |
31d7b702a6 | ||
![]() |
df4caf41d0 | ||
![]() |
0595fc3097 | ||
![]() |
b0dc782c98 | ||
![]() |
ecd7f86df0 | ||
![]() |
b834671555 | ||
![]() |
6e24b52a7e | ||
![]() |
628e12c944 | ||
![]() |
adbec5bffc | ||
![]() |
e8a5306c23 | ||
![]() |
b274b10f38 | ||
![]() |
ac4f2c9f73 | ||
![]() |
97ed7fbb3f | ||
![]() |
003ca655ee | ||
![]() |
412910ca65 | ||
![]() |
31f569ada9 | ||
![]() |
e75c9efb3f | ||
![]() |
e93919673e | ||
![]() |
c814b39fdb | ||
![]() |
a491f97eb9 | ||
![]() |
3c487928d4 | ||
![]() |
e824c553ca | ||
![]() |
2634f35b4e | ||
![]() |
a1aaeab33a | ||
![]() |
e9816f7e30 | ||
![]() |
a9459c6d92 | ||
![]() |
eec67d8b1a | ||
![]() |
e8d9fe0aa8 | ||
![]() |
aa03550f6b | ||
![]() |
61c88db8a1 | ||
![]() |
8dca73d08e | ||
![]() |
3f4ce70414 | ||
![]() |
945afbc6d4 | ||
![]() |
c0a342d790 | ||
![]() |
6de6c10bc3 | ||
![]() |
6c25c9760a | ||
![]() |
7bf140f921 | ||
![]() |
e3d281b3c4 | ||
![]() |
0c43c4b5e1 | ||
![]() |
23dd644f4a | ||
![]() |
7f90a1cab2 | ||
![]() |
b6e0f538c5 | ||
![]() |
8cd138608c | ||
![]() |
846575b7fb | ||
![]() |
3d2f843c1d | ||
![]() |
5ba83d4dfb | ||
![]() |
0dd19ed49c | ||
![]() |
77b83b9e4d | ||
![]() |
7db4eeaf7f | ||
![]() |
7d651e2b7a | ||
![]() |
40c424e793 | ||
![]() |
a6ea5d43b4 | ||
![]() |
bf70e91a0d | ||
![]() |
5cf923ead6 | ||
![]() |
fec2461e0e | ||
![]() |
c71a5643ff | ||
![]() |
b0387c4428 | ||
![]() |
1e149a704b | ||
![]() |
cb71b4a657 | ||
![]() |
26cc41094d | ||
![]() |
9946b19735 | ||
![]() |
6ad9a97f0d | ||
![]() |
a91ad0189e | ||
![]() |
67b6657bcd | ||
![]() |
e1a34c8030 | ||
![]() |
b70f907d25 | ||
![]() |
cde855f67d | ||
![]() |
9cf43dd8ff | ||
![]() |
03e6a92cf3 | ||
![]() |
21c2e8da6e | ||
![]() |
b3963e56ec | ||
![]() |
c6e8e2398c | ||
![]() |
4b5718431d | ||
![]() |
333e1d6789 | ||
![]() |
2e9c71f2c0 | ||
![]() |
072879cc6e | ||
![]() |
cc75adfed6 | ||
![]() |
3cafc1f2c6 | ||
![]() |
19a65f8db6 | ||
![]() |
e8d1d28fdd | ||
![]() |
9ad063ce03 | ||
![]() |
f67693c56c | ||
![]() |
9616fbdc36 | ||
![]() |
48dd5af9e3 | ||
![]() |
bc0fb5e3d9 | ||
![]() |
8e2bbf8c82 | ||
![]() |
538caafac2 | ||
![]() |
0871d6c9c6 | ||
![]() |
468b0e8934 | ||
![]() |
6cbfc63311 | ||
![]() |
2886b217ab | ||
![]() |
fafc68673a | ||
![]() |
1990df63aa | ||
![]() |
e39f0f3e25 | ||
![]() |
1f5e2fa3ce | ||
![]() |
204dd77404 | ||
![]() |
4e5b1ccde6 | ||
![]() |
80844ae2ee | ||
![]() |
a69a00785f | ||
![]() |
41dd70f644 | ||
![]() |
e5b8d5f7ea | ||
![]() |
c49869160b | ||
![]() |
69089da88e | ||
![]() |
e43a733017 | ||
![]() |
3eb6b9d297 | ||
![]() |
ac5ab52d01 | ||
![]() |
0d89b82bff | ||
![]() |
0cde24e103 | ||
![]() |
5598f05dee | ||
![]() |
e932fc832c | ||
![]() |
01b6830fd2 | ||
![]() |
c1d0ac7b9d | ||
![]() |
dce667fa07 | ||
![]() |
a78361341e | ||
![]() |
c87d6e4720 | ||
![]() |
71346760d0 | ||
![]() |
f6c1f336d4 | ||
![]() |
638c958acd | ||
![]() |
b2231945dc | ||
![]() |
4dbfafa8ca | ||
![]() |
56b8da133c | ||
![]() |
06af6f19a3 | ||
![]() |
5f37852695 | ||
![]() |
5fe8a43e36 | ||
![]() |
760b62e068 | ||
![]() |
9205334235 | ||
![]() |
ca4c6ffe8d | ||
![]() |
b47b555c4f | ||
![]() |
5d2f97de74 | ||
![]() |
9e0636eefa | ||
![]() |
6ae1228e61 | ||
![]() |
29311e6391 | ||
![]() |
bd4f66fda3 | ||
![]() |
dc89499116 | ||
![]() |
41b58b8bc1 | ||
![]() |
58df05a7e7 | ||
![]() |
fb940e4269 | ||
![]() |
26fc57d1b3 | ||
![]() |
da57f92796 | ||
![]() |
236820d093 | ||
![]() |
87712b9fa5 | ||
![]() |
510d6d7874 | ||
![]() |
8830054fad | ||
![]() |
327fe63047 | ||
![]() |
0f5c9b4af3 | ||
![]() |
9813396880 | ||
![]() |
f5f86993f1 | ||
![]() |
d4fc22add4 | ||
![]() |
d699a550c8 | ||
![]() |
f71d4312e2 | ||
![]() |
ec777a802c | ||
![]() |
82cad58b8d | ||
![]() |
34231383ec | ||
![]() |
6e14e8ed91 | ||
![]() |
4aedd3a09a | ||
![]() |
26dea0f247 | ||
![]() |
0792e72f71 | ||
![]() |
d9420c1f73 | ||
![]() |
ee1884423a | ||
![]() |
17480a0398 | ||
![]() |
75ec855822 | ||
![]() |
2c5080e382 | ||
![]() |
48e9742658 | ||
![]() |
14b62120fd | ||
![]() |
4a8149627e | ||
![]() |
9c85ba5b66 | ||
![]() |
fb0cb43261 | ||
![]() |
23722dc291 | ||
![]() |
e841f568c1 | ||
![]() |
9b096322e1 | ||
![]() |
df32a81165 | ||
![]() |
8924d657a4 | ||
![]() |
98ba529ead | ||
![]() |
9a01cd84c2 | ||
![]() |
09c6f57364 | ||
![]() |
a807572382 | ||
![]() |
dc6a44d0eb | ||
![]() |
c296e9b9bb | ||
![]() |
d22bb8fc7d | ||
![]() |
b99275f6a5 | ||
![]() |
57502bc911 | ||
![]() |
128e66fa24 | ||
![]() |
0132ac3c27 | ||
![]() |
cfd8d70890 | ||
![]() |
44d2871dc9 | ||
![]() |
821e3beab0 | ||
![]() |
a439e087e1 | ||
![]() |
b8acbf3c3a | ||
![]() |
d25214beb1 | ||
![]() |
22d9bee41a | ||
![]() |
a6eef22fbc | ||
![]() |
f189367c02 | ||
![]() |
40fa4463de | ||
![]() |
729df112a7 | ||
![]() |
9b52b9bf66 | ||
![]() |
c6d5a5a6cc | ||
![]() |
7f169e97ca | ||
![]() |
560161bdbb | ||
![]() |
1761a71338 | ||
![]() |
3da3612c7b | ||
![]() |
198432f222 | ||
![]() |
a868685ac9 | ||
![]() |
d4cab60343 | ||
![]() |
da12ceae5b | ||
![]() |
79b10612aa | ||
![]() |
d5edbb424a | ||
![]() |
d527e2c926 | ||
![]() |
b899dd59c5 | ||
![]() |
8f928982e0 | ||
![]() |
8f243ad59d | ||
![]() |
c9453bab19 | ||
![]() |
78b7ed0ebe | ||
![]() |
d468d0f71b | ||
![]() |
6bc636c2f2 | ||
![]() |
43a6be6471 | ||
![]() |
d9f2a406f6 | ||
![]() |
0bdbf007b2 | ||
![]() |
f1cbb2a0b3 | ||
![]() |
ecfbfb4527 | ||
![]() |
d8690f426c | ||
![]() |
39f2e49451 | ||
![]() |
58f14c5fe2 | ||
![]() |
319ac23736 | ||
![]() |
86e50530b0 | ||
![]() |
7881081207 | ||
![]() |
8623294fcd | ||
![]() |
76537a7f41 | ||
![]() |
d85ae5dcae | ||
![]() |
4e0683565d | ||
![]() |
4d6c07f18a | ||
![]() |
b0c68e0ea7 | ||
![]() |
2858f56d4d | ||
![]() |
c5d443a710 | ||
![]() |
96af0cffc8 | ||
![]() |
114af8e24b | ||
![]() |
14752baf27 | ||
![]() |
f2962a0d16 | ||
![]() |
6ea92f86a5 | ||
![]() |
55997c74b0 | ||
![]() |
4e066f4681 | ||
![]() |
dbc4f285f1 | ||
![]() |
f5da0e341c | ||
![]() |
21c96fa76c | ||
![]() |
c1d441b0ac | ||
![]() |
d63c44f778 | ||
![]() |
03bb3d9ddc | ||
![]() |
9413b5a415 | ||
![]() |
08e2959742 | ||
![]() |
6d9f1b3fd3 | ||
![]() |
a89c8eeabe | ||
![]() |
f382be4c15 | ||
![]() |
ca70b96005 | ||
![]() |
37602647aa | ||
![]() |
d22c3f13b2 | ||
![]() |
024ce0e8eb | ||
![]() |
ee5540f351 | ||
![]() |
e669e1e2bf | ||
![]() |
227b8bdf8a | ||
![]() |
76549beb96 | ||
![]() |
2e848c3f1f | ||
![]() |
266b3bc714 | ||
![]() |
f3e4e8dce8 | ||
![]() |
73008885c8 | ||
![]() |
1460f7bd80 | ||
![]() |
f722a6c08d | ||
![]() |
cb5426c1fa | ||
![]() |
7564d1fb52 | ||
![]() |
a02b69db38 | ||
![]() |
5ab1996d3f | ||
![]() |
ffce593cc8 | ||
![]() |
56155740fe | ||
![]() |
0a13c47a8c | ||
![]() |
d2022cae28 | ||
![]() |
05bb645263 | ||
![]() |
ddeb6b6baa | ||
![]() |
08eca4a237 | ||
![]() |
1e248551d5 | ||
![]() |
c173a3be44 | ||
![]() |
b782ed6bbb | ||
![]() |
a0b1b2e254 | ||
![]() |
c629f24f07 | ||
![]() |
6b3c740dc3 | ||
![]() |
616301f7ee | ||
![]() |
e9b0f54a43 | ||
![]() |
aa8ddeca34 | ||
![]() |
fe8a330a45 | ||
![]() |
f9b3ba2887 | ||
![]() |
50d282ff37 | ||
![]() |
970b00b8d6 | ||
![]() |
92816b57ef | ||
![]() |
9a8b945118 | ||
![]() |
d8f5e9b878 | ||
![]() |
b0e6f34976 | ||
![]() |
9aeb75f28d | ||
![]() |
8951c80225 | ||
![]() |
6c5124e12a | ||
![]() |
08591dae0e | ||
![]() |
6d3c3ce449 | ||
![]() |
3d03a86b13 | ||
![]() |
7e2278f1cc | ||
![]() |
416ff10ba9 | ||
![]() |
aa91211229 | ||
![]() |
cc1de3191f | ||
![]() |
10c8f21f79 | ||
![]() |
3da0f5e384 | ||
![]() |
7260cada90 | ||
![]() |
73d6dc6b6a | ||
![]() |
4627d2c1fb | ||
![]() |
f54ad26630 | ||
![]() |
4c328e4959 | ||
![]() |
1efccf2d90 | ||
![]() |
6badd83c5d | ||
![]() |
b817609adc | ||
![]() |
61f4c73aca | ||
![]() |
24e1a568a2 | ||
![]() |
06ca04c1c8 | ||
![]() |
5698173c76 | ||
![]() |
d7fcb5268a | ||
![]() |
d041c62f55 | ||
![]() |
0eb387916f | ||
![]() |
b87c541d3a | ||
![]() |
a6a3555684 | ||
![]() |
7559e70027 | ||
![]() |
8fcfcc40fc | ||
![]() |
c2218e8a64 | ||
![]() |
0a7919a279 | ||
![]() |
046a4fc401 | ||
![]() |
70bbb867f9 | ||
![]() |
ae5f284d10 | ||
![]() |
6ea0575a4a | ||
![]() |
d88d57f3bb | ||
![]() |
bd80346592 | ||
![]() |
7292f2be69 | ||
![]() |
21d04b3e14 | ||
![]() |
3c6235bee5 | ||
![]() |
b0985bb459 | ||
![]() |
8e93d0a7a2 | ||
![]() |
820b381a8d | ||
![]() |
236c5deeee | ||
![]() |
935240f8c3 | ||
![]() |
168f20bdf4 | ||
![]() |
1810e459ee | ||
![]() |
d86837cc4d | ||
![]() |
af926db211 | ||
![]() |
20ba80f934 | ||
![]() |
34e3d2f997 | ||
![]() |
fadfb89b4c | ||
![]() |
84e6813779 | ||
![]() |
4921d35e70 | ||
![]() |
cebb146e7c | ||
![]() |
4e6b133a17 | ||
![]() |
0a5966c283 | ||
![]() |
3f6a30a974 | ||
![]() |
0db27f1cef | ||
![]() |
628264be4e | ||
![]() |
d286723087 | ||
![]() |
fb3d66e6e1 | ||
![]() |
795300848c | ||
![]() |
b3b2e8ffb7 | ||
![]() |
6a4bf1f817 | ||
![]() |
7c27bab3c7 | ||
![]() |
accfedce87 | ||
![]() |
4cb0ff1f63 | ||
![]() |
896eaba2d6 | ||
![]() |
d648eb1e4f | ||
![]() |
848a2a95a8 | ||
![]() |
9235b52828 | ||
![]() |
3fa84039f8 | ||
![]() |
929f3c2594 | ||
![]() |
95d460c8bd | ||
![]() |
67e87f9048 | ||
![]() |
9924dd7aca | ||
![]() |
3ac8c6d1fe | ||
![]() |
b179dbcdcf | ||
![]() |
282b4f4927 | ||
![]() |
b68a796c7c | ||
![]() |
48276b041c | ||
![]() |
bfafe9ccbe | ||
![]() |
4cb1d77783 | ||
![]() |
787bd75587 | ||
![]() |
dc93779f02 | ||
![]() |
14066dfb5a | ||
![]() |
7d9988fd75 | ||
![]() |
2fed016347 | ||
![]() |
d1b82e9ede | ||
![]() |
b8e20fcadf | ||
![]() |
ebc09017b8 | ||
![]() |
798b72e164 | ||
![]() |
f77514c6f2 | ||
![]() |
7887d6d6e4 | ||
![]() |
0dc0706eb2 | ||
![]() |
b30f4b8fc0 | ||
![]() |
233bc1a108 | ||
![]() |
61dabae6ab | ||
![]() |
d858e1be05 | ||
![]() |
4c3f39be02 | ||
![]() |
b5ada3bf10 | ||
![]() |
a3794b3241 | ||
![]() |
952d72fdd3 | ||
![]() |
5a9db70d24 | ||
![]() |
17b59cd410 | ||
![]() |
eb3e53e2d3 | ||
![]() |
8af0747f95 | ||
![]() |
ceac04b82d | ||
![]() |
e93fbcf701 | ||
![]() |
337cd40cb6 | ||
![]() |
3664f61e2d | ||
![]() |
1acd34313b | ||
![]() |
888c5172bf | ||
![]() |
3d802afecb | ||
![]() |
1647ebaf31 | ||
![]() |
ae1511d8f6 | ||
![]() |
85f4cecc64 | ||
![]() |
203c3a5175 | ||
![]() |
846d31c4f1 | ||
![]() |
cb460a85ba | ||
![]() |
592d30d495 | ||
![]() |
a79224aba8 | ||
![]() |
7c5da67d74 | ||
![]() |
b71baef7c8 | ||
![]() |
2c341f2a65 | ||
![]() |
1c1363875c | ||
![]() |
156ab7dc2b | ||
![]() |
4db0e7888a | ||
![]() |
e98054accb | ||
![]() |
7771ecfe58 | ||
![]() |
6cd9667364 | ||
![]() |
bf7e09ce59 | ||
![]() |
32844bb318 | ||
![]() |
1bca313421 | ||
![]() |
984d41e334 | ||
![]() |
fcfbdd2d89 | ||
![]() |
4ec2af785a | ||
![]() |
0eba920075 | ||
![]() |
8f4bb8d445 | ||
![]() |
3b8f254dfd | ||
![]() |
64d6fa8e86 | ||
![]() |
3b4a9a337b | ||
![]() |
ae1bcd5fef | ||
![]() |
9fb1f2fa17 | ||
![]() |
d261c6ccc1 | ||
![]() |
9ca5bdda7f | ||
![]() |
6cc1bf37cc | ||
![]() |
f5db7707bb | ||
![]() |
859ae2fbad | ||
![]() |
96a51d16a7 | ||
![]() |
09292d5918 | ||
![]() |
f62d473fc4 | ||
![]() |
607b44f7c0 | ||
![]() |
d78e132007 | ||
![]() |
8d3c9bc2d0 | ||
![]() |
6d4545cb3e | ||
![]() |
c311e480fd | ||
![]() |
4c6ddd435c | ||
![]() |
d31140f8cd | ||
![]() |
0ed9e185b2 | ||
![]() |
408ae44bdd | ||
![]() |
bf9c2c74fa | ||
![]() |
ce93a332a7 | ||
![]() |
bc15f11473 | ||
![]() |
fccbd41203 | ||
![]() |
17b3d3a8e4 | ||
![]() |
279192d317 | ||
![]() |
701d258076 | ||
![]() |
034bbb4f5f | ||
![]() |
2943ad15a5 | ||
![]() |
13c3833593 | ||
![]() |
d0715c75c0 | ||
![]() |
eca424656a | ||
![]() |
3b60081e2a | ||
![]() |
fbfaa41cb0 | ||
![]() |
df1da7554c | ||
![]() |
6d280084fb | ||
![]() |
1096fe3d87 | ||
![]() |
389da16947 | ||
![]() |
185af1b42a | ||
![]() |
d17f27b65c | ||
![]() |
bb0867f1a8 | ||
![]() |
ac788a7ee7 | ||
![]() |
bf52aa8ccc | ||
![]() |
d7c8adc085 | ||
![]() |
8b4ef3bbdd | ||
![]() |
b67d32824c | ||
![]() |
14c0ada9ac | ||
![]() |
618039734a | ||
![]() |
0d5e151c60 | ||
![]() |
bad920fa87 | ||
![]() |
281fe93a26 | ||
![]() |
4a71593ffd | ||
![]() |
014cc14b7e | ||
![]() |
ee71d2ca60 | ||
![]() |
5085ce8ab1 | ||
![]() |
9ed5b70d01 | ||
![]() |
976bf3e979 | ||
![]() |
0b70419859 | ||
![]() |
6f903db8c4 | ||
![]() |
4c88578371 | ||
![]() |
b1dcfaf6b3 | ||
![]() |
449a7d3fd5 | ||
![]() |
7fd2e67d11 | ||
![]() |
34260ed09f | ||
![]() |
a00d8a493d | ||
![]() |
263c0322ee | ||
![]() |
2b0e56932b | ||
![]() |
e12cef8d77 | ||
![]() |
704cdac874 | ||
![]() |
89d7c0af91 | ||
![]() |
5f3bcedbba | ||
![]() |
d2d3f27f85 | ||
![]() |
6795db9bd6 | ||
![]() |
6a693546a3 | ||
![]() |
a8c73ffb93 | ||
![]() |
411e36b0f8 | ||
![]() |
fbfc674ca5 | ||
![]() |
ca20b0cf17 | ||
![]() |
05454b76a6 | ||
![]() |
b4c858bcdf | ||
![]() |
4d4fd19f87 | ||
![]() |
16a846b1e7 | ||
![]() |
034b0e07d2 | ||
![]() |
c486f794f9 | ||
![]() |
9220270948 | ||
![]() |
22f68d70a7 | ||
![]() |
bf85e18d45 | ||
![]() |
09c43e8854 | ||
![]() |
7be7d3ffac | ||
![]() |
2823ef84db | ||
![]() |
4d07448cf8 | ||
![]() |
12d59797a7 | ||
![]() |
673290d2e1 | ||
![]() |
5a81ddd4e7 | ||
![]() |
ef820c3126 | ||
![]() |
278b9d0f71 | ||
![]() |
276ab191b5 | ||
![]() |
e5cbf01ce1 | ||
![]() |
fe2e5089ab | ||
![]() |
35ffac1e01 | ||
![]() |
362f23a950 | ||
![]() |
dc8d4ac8e4 | ||
![]() |
0cdea28e2a | ||
![]() |
7d1a02feb1 | ||
![]() |
b90636f640 | ||
![]() |
b4374c8c4c | ||
![]() |
3076866ec6 | ||
![]() |
e6a54013dc | ||
![]() |
3edc58a04e | ||
![]() |
70fe4f22db | ||
![]() |
9f1dc71320 | ||
![]() |
f43eca248a | ||
![]() |
958b894020 | ||
![]() |
0ba2b4e253 | ||
![]() |
1e6b91b05a | ||
![]() |
5c8f209aa7 | ||
![]() |
d966e0cfce | ||
![]() |
3eeccc1a65 | ||
![]() |
52e33c2aa2 | ||
![]() |
35f5784287 | ||
![]() |
46cc6e199b | ||
![]() |
6371eca14d | ||
![]() |
052641e620 | ||
![]() |
16edcd9938 | ||
![]() |
4fa6f2e54f | ||
![]() |
3c1cdecb88 | ||
![]() |
18286dbf4b | ||
![]() |
3a0616c680 | ||
![]() |
440e4289e4 | ||
![]() |
8fe1a84db2 | ||
![]() |
5fa66ba4a3 | ||
![]() |
261f3bcba6 | ||
![]() |
9be1b72ed7 | ||
![]() |
5610541515 | ||
![]() |
bfc8d2457c | ||
![]() |
dedc2ef918 | ||
![]() |
a9c85b9944 | ||
![]() |
bf91a8c1b3 | ||
![]() |
6f299e7245 | ||
![]() |
1ad495070d | ||
![]() |
84719d944a | ||
![]() |
4ca588deae | ||
![]() |
325001933d | ||
![]() |
acc9fd0382 | ||
![]() |
f32d1c0dea | ||
![]() |
ca89d6184c | ||
![]() |
2bfe7aa219 | ||
![]() |
6fcd56c462 | ||
![]() |
1ce2d97d3d | ||
![]() |
04c5cda7e5 | ||
![]() |
7692cffdbe | ||
![]() |
7c093bd928 | ||
![]() |
bcee3f9570 | ||
![]() |
78ffb6f3e6 | ||
![]() |
d1aa4f42e5 | ||
![]() |
e7d34913c0 | ||
![]() |
1a3a38d370 | ||
![]() |
59ce31f44f | ||
![]() |
b3d8f8620c | ||
![]() |
3eebb9d51d | ||
![]() |
b6bb6919e6 | ||
![]() |
c08862679d | ||
![]() |
50db622689 | ||
![]() |
9303a56d8f | ||
![]() |
6667138b73 | ||
![]() |
d9852bc75d | ||
![]() |
6aeccf0330 | ||
![]() |
f8572c1d71 | ||
![]() |
e2e001d042 | ||
![]() |
e3307213b1 | ||
![]() |
84baaa324c | ||
![]() |
42ee8eef50 | ||
![]() |
3fef9a93cf | ||
![]() |
4b256f3466 | ||
![]() |
bebfc3d16e | ||
![]() |
fd3902f7e7 | ||
![]() |
dfb992adb2 | ||
![]() |
a252065f99 | ||
![]() |
6947f8cb2e | ||
![]() |
85dfea1642 | ||
![]() |
015c8811a5 | ||
![]() |
d9c78b77cb | ||
![]() |
1b543cf538 | ||
![]() |
9fb8144031 | ||
![]() |
f2033c418f | ||
![]() |
aa266cb630 | ||
![]() |
9a5d783537 | ||
![]() |
5800b57791 | ||
![]() |
c840771c0a | ||
![]() |
9678752480 | ||
![]() |
31b2f331db | ||
![]() |
0ba54ee9b7 | ||
![]() |
5c86a51b45 | ||
![]() |
9debbfb1a8 | ||
![]() |
97b671171b | ||
![]() |
179fb0f3b5 | ||
![]() |
96b7bb625d | ||
![]() |
afeb13d980 | ||
![]() |
6e1728542e | ||
![]() |
9438dd1cbd | ||
![]() |
0194905e97 | ||
![]() |
25505dc1d4 | ||
![]() |
ce219ac6c7 | ||
![]() |
fa20957e01 | ||
![]() |
7959c04d1e | ||
![]() |
e6d7f6ed71 | ||
![]() |
144b530045 | ||
![]() |
39ba99005a | ||
![]() |
f867b025e5 | ||
![]() |
c928f82cbf | ||
![]() |
9d7aa8f05d | ||
![]() |
02f927ae2d | ||
![]() |
1d022522cd | ||
![]() |
bad9ac5395 | ||
![]() |
e9f561e7ab | ||
![]() |
14d169558f | ||
![]() |
ca2a68217d | ||
![]() |
0a9a8ecc4e | ||
![]() |
6cef850497 | ||
![]() |
66af4bd011 | ||
![]() |
03253f4598 | ||
![]() |
aa5d8e5a81 | ||
![]() |
206029eadc | ||
![]() |
958c5ecbfe | ||
![]() |
3d79bf2bfe | ||
![]() |
1de0a0bbb9 | ||
![]() |
8d22479d24 | ||
![]() |
7f7435f003 | ||
![]() |
d2eb5bb0f3 | ||
![]() |
085303c349 | ||
![]() |
9ac6f906ff | ||
![]() |
f995ab9d54 | ||
![]() |
77f595c9a4 | ||
![]() |
8d0b1588be | ||
![]() |
70c5c82541 | ||
![]() |
bf910ef383 | ||
![]() |
99c49c0993 | ||
![]() |
f6e6c21ba6 | ||
![]() |
41b7f5ab1c | ||
![]() |
c5bd6b3d6b | ||
![]() |
9e96397e6a | ||
![]() |
f207e01510 | ||
![]() |
6b3bb3347b | ||
![]() |
806903ffe0 | ||
![]() |
fdf1fa48e3 | ||
![]() |
636077c74d | ||
![]() |
e047e4dcff | ||
![]() |
eae306c3f1 | ||
![]() |
fbd7c72283 | ||
![]() |
fc58746bc3 | ||
![]() |
9ae878d8f2 | ||
![]() |
afe9fc221e | ||
![]() |
eb912be47a | ||
![]() |
5c346e8fb6 | ||
![]() |
8d388c5e79 | ||
![]() |
314574fc84 | ||
![]() |
e356d0bcda | ||
![]() |
f991ec15f2 | ||
![]() |
d7d83c683d | ||
![]() |
ff867a7d57 | ||
![]() |
1282370ccb | ||
![]() |
eebd094423 | ||
![]() |
57bd4185d4 | ||
![]() |
91ba35c68e | ||
![]() |
a99e15343c | ||
![]() |
4583638b92 | ||
![]() |
10a1b156e3 | ||
![]() |
a8286535eb | ||
![]() |
05146badf1 | ||
![]() |
c483e4479e | ||
![]() |
4a70c725b4 | ||
![]() |
33ed017851 | ||
![]() |
fffc4dd3fd | ||
![]() |
e072981295 | ||
![]() |
5d983d0b61 | ||
![]() |
727f667cbc | ||
![]() |
1b4fc2ae8d | ||
![]() |
5b0d1415ad | ||
![]() |
edf34eea94 | ||
![]() |
a303f67d3b | ||
![]() |
1b5f526e09 | ||
![]() |
03a0a3572b | ||
![]() |
297d24c5b0 | ||
![]() |
c8cf06b8b7 | ||
![]() |
49d6d7c656 | ||
![]() |
96fd874090 | ||
![]() |
c9703872e2 | ||
![]() |
2f5d7d4522 | ||
![]() |
7716e8fb68 | ||
![]() |
c2fc8a0d61 | ||
![]() |
9be384690a | ||
![]() |
692eeb3687 | ||
![]() |
213c91ae73 | ||
![]() |
36b1a89f93 | ||
![]() |
6b359c95da | ||
![]() |
1fec64a1b3 | ||
![]() |
70ed58a78d | ||
![]() |
7a4238095d | ||
![]() |
cf89f45697 | ||
![]() |
2dc78e6f0c | ||
![]() |
9da74dda43 | ||
![]() |
18149dcb8c | ||
![]() |
94a2fd542e | ||
![]() |
6fa8556033 | ||
![]() |
19cfa8cf22 | ||
![]() |
a859997190 | ||
![]() |
6f9860b25e | ||
![]() |
128ce589e1 | ||
![]() |
9b21774392 | ||
![]() |
eaf4a75402 | ||
![]() |
a1a6d4a631 | ||
![]() |
de1fd5a7fa | ||
![]() |
0d96095646 | ||
![]() |
45085dd97f | ||
![]() |
b2a1204bc5 | ||
![]() |
990a9e80a2 | ||
![]() |
0ffcc197d4 | ||
![]() |
1a051f038d | ||
![]() |
1e22c8daca | ||
![]() |
b8cbd39985 | ||
![]() |
3508622e3b | ||
![]() |
b8f6d824fd | ||
![]() |
e687848152 | ||
![]() |
2a9fd9ae26 | ||
![]() |
3ec4070d8c | ||
![]() |
6f8038992c | ||
![]() |
5c9a58f3e6 | ||
![]() |
d34214ad32 | ||
![]() |
2b7021407c | ||
![]() |
03cd4480df | ||
![]() |
910825580e | ||
![]() |
c8d479e594 | ||
![]() |
34f6245e74 | ||
![]() |
e9ea5c2ccb | ||
![]() |
4347a0f6b7 | ||
![]() |
5888e32360 | ||
![]() |
4214a354a7 | ||
![]() |
369afd7ddd | ||
![]() |
281445917b | ||
![]() |
df6846344d | ||
![]() |
05960fa29c | ||
![]() |
068749bcbe | ||
![]() |
f21f32778f | ||
![]() |
8ef3c6d4d3 | ||
![]() |
c7a78ed522 | ||
![]() |
45adb5c9c7 | ||
![]() |
4004867eda | ||
![]() |
118d3bc11c | ||
![]() |
0e9d71f232 | ||
![]() |
b552fbe312 |
35
.coveragerc
35
.coveragerc
@@ -13,6 +13,10 @@ omit =
|
||||
homeassistant/components/abode/*
|
||||
homeassistant/components/acer_projector/switch.py
|
||||
homeassistant/components/actiontec/device_tracker.py
|
||||
homeassistant/components/adguard/__init__.py
|
||||
homeassistant/components/adguard/const.py
|
||||
homeassistant/components/adguard/sensor.py
|
||||
homeassistant/components/adguard/switch.py
|
||||
homeassistant/components/ads/*
|
||||
homeassistant/components/aftership/sensor.py
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
@@ -34,6 +38,8 @@ omit =
|
||||
homeassistant/components/apple_tv/*
|
||||
homeassistant/components/aqualogic/*
|
||||
homeassistant/components/aquostv/media_player.py
|
||||
homeassistant/components/arcam_fmj/media_player.py
|
||||
homeassistant/components/arcam_fmj/__init__.py
|
||||
homeassistant/components/arduino/*
|
||||
homeassistant/components/arest/binary_sensor.py
|
||||
homeassistant/components/arest/sensor.py
|
||||
@@ -45,8 +51,10 @@ omit =
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/asuswrt/device_tracker.py
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/automatic/device_tracker.py
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/azure_event_hub/*
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/bbb_gpio/*
|
||||
homeassistant/components/bbox/device_tracker.py
|
||||
@@ -152,6 +160,7 @@ omit =
|
||||
homeassistant/components/eight_sleep/*
|
||||
homeassistant/components/eliqonline/sensor.py
|
||||
homeassistant/components/elkm1/*
|
||||
homeassistant/components/elv/switch.py
|
||||
homeassistant/components/emby/media_player.py
|
||||
homeassistant/components/emoncms/sensor.py
|
||||
homeassistant/components/emoncms_history/*
|
||||
@@ -160,6 +169,7 @@ omit =
|
||||
homeassistant/components/enocean/*
|
||||
homeassistant/components/enphase_envoy/sensor.py
|
||||
homeassistant/components/entur_public_transport/*
|
||||
homeassistant/components/environment_canada/*
|
||||
homeassistant/components/envirophat/sensor.py
|
||||
homeassistant/components/envisalink/*
|
||||
homeassistant/components/ephember/climate.py
|
||||
@@ -171,6 +181,7 @@ omit =
|
||||
homeassistant/components/esphome/camera.py
|
||||
homeassistant/components/esphome/climate.py
|
||||
homeassistant/components/esphome/cover.py
|
||||
homeassistant/components/esphome/entry_data.py
|
||||
homeassistant/components/esphome/fan.py
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/sensor.py
|
||||
@@ -206,6 +217,7 @@ omit =
|
||||
homeassistant/components/fritzbox_callmonitor/sensor.py
|
||||
homeassistant/components/fritzbox_netmonitor/sensor.py
|
||||
homeassistant/components/fritzdect/switch.py
|
||||
homeassistant/components/fronius/sensor.py
|
||||
homeassistant/components/frontier_silicon/media_player.py
|
||||
homeassistant/components/futurenow/light.py
|
||||
homeassistant/components/garadget/cover.py
|
||||
@@ -221,6 +233,7 @@ omit =
|
||||
homeassistant/components/goalfeed/*
|
||||
homeassistant/components/gogogate2/cover.py
|
||||
homeassistant/components/google/*
|
||||
homeassistant/components/google_cloud/tts.py
|
||||
homeassistant/components/google_maps/device_tracker.py
|
||||
homeassistant/components/google_travel_time/sensor.py
|
||||
homeassistant/components/googlehome/*
|
||||
@@ -250,7 +263,6 @@ omit =
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
homeassistant/components/hive/*
|
||||
homeassistant/components/hlk_sw16/*
|
||||
homeassistant/components/homekit_controller/*
|
||||
homeassistant/components/homematic/*
|
||||
homeassistant/components/homematic/climate.py
|
||||
homeassistant/components/homematic/cover.py
|
||||
@@ -311,6 +323,7 @@ omit =
|
||||
homeassistant/components/lcn/*
|
||||
homeassistant/components/lg_netcast/media_player.py
|
||||
homeassistant/components/lg_soundbar/media_player.py
|
||||
homeassistant/components/life360/*
|
||||
homeassistant/components/lifx/*
|
||||
homeassistant/components/lifx_cloud/scene.py
|
||||
homeassistant/components/lifx_legacy/light.py
|
||||
@@ -344,6 +357,7 @@ omit =
|
||||
homeassistant/components/mastodon/notify.py
|
||||
homeassistant/components/matrix/*
|
||||
homeassistant/components/maxcube/*
|
||||
homeassistant/components/mcp23017/*
|
||||
homeassistant/components/media_extractor/*
|
||||
homeassistant/components/mediaroom/media_player.py
|
||||
homeassistant/components/message_bird/notify.py
|
||||
@@ -395,6 +409,8 @@ omit =
|
||||
homeassistant/components/nissan_leaf/*
|
||||
homeassistant/components/nmap_tracker/device_tracker.py
|
||||
homeassistant/components/nmbs/sensor.py
|
||||
homeassistant/components/notion/binary_sensor.py
|
||||
homeassistant/components/notion/sensor.py
|
||||
homeassistant/components/noaa_tides/sensor.py
|
||||
homeassistant/components/norway_air/air_quality.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.py
|
||||
@@ -442,6 +458,7 @@ omit =
|
||||
homeassistant/components/ping/device_tracker.py
|
||||
homeassistant/components/pioneer/media_player.py
|
||||
homeassistant/components/pjlink/media_player.py
|
||||
homeassistant/components/plaato/*
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plex/sensor.py
|
||||
homeassistant/components/plum_lightpad/*
|
||||
@@ -487,6 +504,9 @@ omit =
|
||||
homeassistant/components/reddit/*
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/repetier/__init__.py
|
||||
homeassistant/components/repetier/sensor.py
|
||||
homeassistant/components/remote_rpi_gpio/*
|
||||
homeassistant/components/rest/binary_sensor.py
|
||||
homeassistant/components/rest/notify.py
|
||||
homeassistant/components/rest/switch.py
|
||||
@@ -539,12 +559,17 @@ omit =
|
||||
homeassistant/components/slack/notify.py
|
||||
homeassistant/components/sma/sensor.py
|
||||
homeassistant/components/smappee/*
|
||||
homeassistant/components/smarty/*
|
||||
homeassistant/components/smarthab/*
|
||||
homeassistant/components/smtp/notify.py
|
||||
homeassistant/components/snapcast/media_player.py
|
||||
homeassistant/components/snmp/*
|
||||
homeassistant/components/sochain/sensor.py
|
||||
homeassistant/components/socialblade/sensor.py
|
||||
homeassistant/components/solaredge/sensor.py
|
||||
homeassistant/components/solaredge_local/sensor.py
|
||||
homeassistant/components/solax/sensor.py
|
||||
homeassistant/components/somfy/*
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/media_player.py
|
||||
@@ -560,6 +585,7 @@ omit =
|
||||
homeassistant/components/starlingbank/sensor.py
|
||||
homeassistant/components/steam_online/sensor.py
|
||||
homeassistant/components/stiebel_eltron/*
|
||||
homeassistant/components/streamlabswater/*
|
||||
homeassistant/components/stride/notify.py
|
||||
homeassistant/components/supervisord/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
@@ -613,9 +639,11 @@ omit =
|
||||
homeassistant/components/tplink/switch.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/traccar/device_tracker.py
|
||||
homeassistant/components/traccar/const.py
|
||||
homeassistant/components/trackr/device_tracker.py
|
||||
homeassistant/components/tradfri/*
|
||||
homeassistant/components/tradfri/light.py
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
homeassistant/components/transmission/*
|
||||
homeassistant/components/travisci/sensor.py
|
||||
@@ -634,6 +662,7 @@ omit =
|
||||
homeassistant/components/uptimerobot/binary_sensor.py
|
||||
homeassistant/components/uscis/sensor.py
|
||||
homeassistant/components/usps/*
|
||||
homeassistant/components/vallox/*
|
||||
homeassistant/components/vasttrafik/sensor.py
|
||||
homeassistant/components/velbus/*
|
||||
homeassistant/components/velux/*
|
||||
@@ -644,6 +673,7 @@ omit =
|
||||
homeassistant/components/viaggiatreno/sensor.py
|
||||
homeassistant/components/vizio/media_player.py
|
||||
homeassistant/components/vlc/media_player.py
|
||||
homeassistant/components/vlc_telnet/media_player.py
|
||||
homeassistant/components/volkszaehler/sensor.py
|
||||
homeassistant/components/volumio/media_player.py
|
||||
homeassistant/components/volvooncall/*
|
||||
@@ -651,6 +681,7 @@ omit =
|
||||
homeassistant/components/waqi/sensor.py
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
homeassistant/components/watson_tts/tts.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
@@ -661,6 +692,8 @@ omit =
|
||||
homeassistant/components/worldtidesinfo/sensor.py
|
||||
homeassistant/components/worxlandroid/sensor.py
|
||||
homeassistant/components/wunderlist/*
|
||||
homeassistant/components/wwlln/__init__.py
|
||||
homeassistant/components/wwlln/geo_location.py
|
||||
homeassistant/components/x10/light.py
|
||||
homeassistant/components/xbox_live/sensor.py
|
||||
homeassistant/components/xeoma/camera.py
|
||||
|
30
.devcontainer/Dockerfile
Normal file
30
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM python:3.7
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
libavdevice-dev \
|
||||
libavutil-dev \
|
||||
libswscale-dev \
|
||||
libswresample-dev \
|
||||
libavfilter-dev \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& cd hass-release \
|
||||
&& pip3 install -e .
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install Python dependencies from requirements.txt if it exists
|
||||
COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspace/
|
||||
RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
24
.devcontainer/devcontainer.json
Normal file
24
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
{
|
||||
"name": "Home Assistant Dev",
|
||||
"context": "..",
|
||||
"dockerFile": "Dockerfile",
|
||||
"postCreateCommand": "pip3 install -e .",
|
||||
"appPort": 8123,
|
||||
"runArgs": [
|
||||
"-e", "GIT_EDTIOR='code --wait'"
|
||||
],
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-azure-devops.azure-pipelines",
|
||||
"redhat.vscode-yaml"
|
||||
],
|
||||
"settings": {
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.rulers": [80],
|
||||
"terminal.integrated.shell.linux": "/bin/bash"
|
||||
}
|
||||
}
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,7 +3,7 @@
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- Do not report issues for integrations if you are using custom integration: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -9,7 +9,7 @@ about: Create a report to help us improve
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues
|
||||
- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- Do not report issues for integrations if you are using a custom integration: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
27
.github/lock.yml
vendored
Normal file
27
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 1
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: 2019-07-01
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: false
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: false
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: pulls
|
||||
|
||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||
issues:
|
||||
daysUntilLock: 30
|
54
.github/stale.yml
vendored
Normal file
54
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- under investigation
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
There hasn't been any activity on this issue recently. Due to the high number
|
||||
of incoming GitHub notifications, we have to clean some of the old issues,
|
||||
as many of them have already been resolved with the latest updates.
|
||||
|
||||
Please make sure to update to the latest Home Assistant version and check
|
||||
if that solves the issue. Let us know if that works for you by adding a
|
||||
comment 👍
|
||||
|
||||
This issue now has been marked as stale and will be closed if no further
|
||||
activity occurs. Thank you for your contributions.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
# closeComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,6 +4,10 @@ config2/*
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
|
||||
# hass-release
|
||||
data/
|
||||
.token
|
||||
|
||||
# Hide sublime text stuff
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
@@ -94,7 +98,10 @@ virtualization/vagrant/.vagrant
|
||||
virtualization/vagrant/config
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/cSpell.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
# Built docs
|
||||
docs/build
|
||||
|
92
.vscode/tasks.json
vendored
Normal file
92
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Preview",
|
||||
"type": "shell",
|
||||
"command": "hass -c ./config",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Pytest",
|
||||
"type": "shell",
|
||||
"command": "pytest --timeout=10 tests",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Flake8",
|
||||
"type": "shell",
|
||||
"command": "flake8 homeassistant tests",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Pylint",
|
||||
"type": "shell",
|
||||
"command": "pylint homeassistant",
|
||||
"dependsOn": [
|
||||
"Install all Requirements"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Generate Requirements",
|
||||
"type": "shell",
|
||||
"command": "./script/gen_requirements_all.py",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
54
CODEOWNERS
54
CODEOWNERS
@@ -17,6 +17,7 @@ virtualization/Docker/* @home-assistant/docker
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
# Integrations
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
@@ -24,14 +25,19 @@ homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/aprs/* @PhilRW
|
||||
homeassistant/components/arcam_fmj/* @elupus
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automatic/* @armills
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/awair/* @danielsjf
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
@@ -40,11 +46,12 @@ homeassistant/components/braviatv/* @robbiet480
|
||||
homeassistant/components/broadlink/* @danielhiversen
|
||||
homeassistant/components/brunt/* @eavanvalkenburg
|
||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||
homeassistant/components/buienradar/* @ties
|
||||
homeassistant/components/cisco_ios/* @fbradyirl
|
||||
homeassistant/components/cisco_mobility_express/* @fbradyirl
|
||||
homeassistant/components/cisco_webex_teams/* @fbradyirl
|
||||
homeassistant/components/ciscospark/* @fbradyirl
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/cloud/* @home-assistant/cloud
|
||||
homeassistant/components/cloudflare/* @ludeeus
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator/* @home-assistant/core
|
||||
@@ -58,6 +65,7 @@ homeassistant/components/daikin/* @fredrike @rofrantz
|
||||
homeassistant/components/darksky/* @fabaff
|
||||
homeassistant/components/deconz/* @kane610
|
||||
homeassistant/components/demo/* @home-assistant/core
|
||||
homeassistant/components/device_automation/* @home-assistant/core
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/doorbird/* @oblogic7
|
||||
@@ -66,9 +74,11 @@ homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/edp_redy/* @abmantis
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elv/* @majuss
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/enocean/* @bdurrer
|
||||
homeassistant/components/environment_canada/* @michaeldavie
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||
homeassistant/components/eq3btsmart/* @rytilahti
|
||||
@@ -83,12 +93,14 @@ homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/google_translate/* @awarecan
|
||||
homeassistant/components/google_travel_time/* @robbiet480
|
||||
homeassistant/components/googlehome/* @ludeeus
|
||||
@@ -104,9 +116,9 @@ homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/history_graph/* @andrey-git
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/honeywell/* @zxdavb
|
||||
homeassistant/components/html5/* @robbiet480
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/huawei_lte/* @scop
|
||||
@@ -131,21 +143,26 @@ homeassistant/components/kodi/* @armills
|
||||
homeassistant/components/konnected/* @heythisisnate
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/lifx/* @amelchio
|
||||
homeassistant/components/lifx_cloud/* @amelchio
|
||||
homeassistant/components/lifx_legacy/* @amelchio
|
||||
homeassistant/components/linky/* @tiste @Quentame
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/liveboxplaytv/* @pschmitt
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/lovelace/* @home-assistant/frontend
|
||||
homeassistant/components/luci/* @fbradyirl
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mcp23017/* @jardiamj
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
homeassistant/components/met/* @danielhiversen
|
||||
homeassistant/components/meteo_france/* @victorcerutti @oncleben31
|
||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/mill/* @danielhiversen
|
||||
@@ -165,31 +182,36 @@ homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
homeassistant/components/notify/* @home-assistant/core
|
||||
homeassistant/components/notion/* @bachya
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/opentherm_gw/* @mvn23
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/owlet/* @oblogic7
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/panel_custom/* @home-assistant/frontend
|
||||
homeassistant/components/panel_iframe/* @home-assistant/frontend
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/philips_js/* @elupus
|
||||
homeassistant/components/pi_hole/* @fabaff
|
||||
homeassistant/components/plaato/* @JohNan
|
||||
homeassistant/components/plant/* @ChristianKuehnel
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/ps4/* @ktnrg45
|
||||
homeassistant/components/ptvsd/* @swamp-ig
|
||||
homeassistant/components/push/* @dgomes
|
||||
homeassistant/components/pvoutput/* @fabaff
|
||||
homeassistant/components/qld_bushfire/* @exxamalte
|
||||
homeassistant/components/qnap/* @colinodell
|
||||
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
||||
homeassistant/components/qwikswitch/* @kellerza
|
||||
homeassistant/components/raincloud/* @vanstinator
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rfxtrx/* @danielhiversen
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roomba/* @pschmitt
|
||||
@@ -197,6 +219,7 @@ homeassistant/components/ruter/* @ludeeus
|
||||
homeassistant/components/scene/* @home-assistant/core
|
||||
homeassistant/components/scrape/* @fabaff
|
||||
homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/sense/* @kbickar
|
||||
homeassistant/components/sensibo/* @andrey-git
|
||||
homeassistant/components/serial/* @fabaff
|
||||
homeassistant/components/seventeentrack/* @bachya
|
||||
@@ -205,15 +228,21 @@ homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smarthab/* @outadoc
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
homeassistant/components/smarty/* @z0mbieprocess
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
homeassistant/components/solaredge_local/* @drobtravels
|
||||
homeassistant/components/solax/* @squishykid
|
||||
homeassistant/components/somfy/* @tetienne
|
||||
homeassistant/components/sonos/* @amelchio
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/sun/* @home-assistant/core
|
||||
homeassistant/components/stream/* @hunterjm
|
||||
homeassistant/components/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
@@ -239,10 +268,10 @@ homeassistant/components/toon/* @frenck
|
||||
homeassistant/components/tplink/* @rytilahti
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/tts/* @robbiet480
|
||||
homeassistant/components/twilio_call/* @robbiet480
|
||||
homeassistant/components/twilio_sms/* @robbiet480
|
||||
homeassistant/components/uber/* @robbiet480
|
||||
homeassistant/components/unifi/* @kane610
|
||||
homeassistant/components/upcloud/* @scop
|
||||
homeassistant/components/updater/* @home-assistant/core
|
||||
@@ -252,28 +281,31 @@ homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/wwlln/* @bachya
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xiaomi_tv/* @simse
|
||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
homeassistant/components/yessssms/* @flowolf
|
||||
homeassistant/components/yi/* @bachya
|
||||
homeassistant/components/zeroconf/* @robbiet480
|
||||
homeassistant/components/yr/* @danielhiversen
|
||||
homeassistant/components/zeroconf/* @robbiet480 @Kane610
|
||||
homeassistant/components/zha/* @dmulcahey @adminiuga
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
|
||||
# Individual files
|
||||
homeassistant/components/group/cover @cdce8p
|
||||
homeassistant/components/demo/weather @fabaff
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
|
||||
# This way, the development image and the production image are kept in sync.
|
||||
|
||||
FROM python:3.7
|
||||
FROM python:3.7-stretch
|
||||
LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
|
||||
# Uncomment any of the following lines to disable the installation.
|
||||
@@ -24,12 +24,14 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython tensorflow
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8123
|
||||
EXPOSE 8300
|
||||
EXPOSE 51827
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Home Assistant |Build Status| |CI Status| |Coverage Status| |Chat Status|
|
||||
Home Assistant |Chat Status|
|
||||
=================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
@@ -27,12 +27,6 @@ components <https://developers.home-assistant.io/docs/en/creating_component_inde
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=dev
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
.. |CI Status| image:: https://circleci.com/gh/home-assistant/home-assistant.svg?style=shield
|
||||
:target: https://circleci.com/gh/home-assistant/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
|
177
azure-pipelines-ci.yml
Normal file
177
azure-pipelines-ci.yml
Normal file
@@ -0,0 +1,177 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
pr: none
|
||||
|
||||
resources:
|
||||
containers:
|
||||
- container: 35
|
||||
image: homeassistant/ci-azure:3.5
|
||||
- container: 36
|
||||
image: homeassistant/ci-azure:3.6
|
||||
- container: 37
|
||||
image: homeassistant/ci-azure:3.7
|
||||
variables:
|
||||
- name: ArtifactFeed
|
||||
value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d'
|
||||
- name: PythonMain
|
||||
value: '35'
|
||||
|
||||
stages:
|
||||
|
||||
- stage: 'Overview'
|
||||
jobs:
|
||||
- job: 'Lint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install flake8
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
flake8 homeassistant tests script
|
||||
displayName: 'Run flake8'
|
||||
- job: 'Validate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
python -m script.hassfest validate
|
||||
displayName: 'Validate manifests'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
./script/gen_requirements_all.py validate
|
||||
displayName: 'requirements_all validate'
|
||||
|
||||
- stage: 'Tests'
|
||||
dependsOn:
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'PyTest'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
Python35:
|
||||
python.container: '35'
|
||||
Python36:
|
||||
python.container: '36'
|
||||
Python37:
|
||||
python.container: '37'
|
||||
container: $[ variables['python.container'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python --version > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install pytest-azurepipelines -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_test_all.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pytest --timeout=9 --durations=10 --junitxml=junit/test-results.xml -qq -o console_output_style=count -p no:sugar tests
|
||||
displayName: 'Run pytest for python $(python.version)'
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/test-*.xml'
|
||||
testRunTitle: 'Publish test results for Python $(python.version)'
|
||||
|
||||
- stage: 'FullCheck'
|
||||
dependsOn:
|
||||
- 'Overview'
|
||||
jobs:
|
||||
- job: 'Pytlint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python --version > .cache
|
||||
displayName: 'Set python $(python.version) for requirement cache'
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: 'Restore artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
set -e
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
displayName: 'Create Virtual Environment & Install Requirements'
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: 'Save artifacts based on Requirements'
|
||||
inputs:
|
||||
keyfile: 'requirements_all.txt, requirements_test.txt, .cache'
|
||||
targetfolder: './venv'
|
||||
vstsFeed: '$(ArtifactFeed)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pip install -e .
|
||||
displayName: 'Install Home Assistant for python $(python.version)'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pylint homeassistant
|
||||
displayName: 'Run pylint'
|
||||
- job: 'Mypy'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -r requirements_test.txt
|
||||
displayName: 'Setup Env'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
TYPING_FILES=$(cat mypyrc)
|
||||
mypy $TYPING_FILES
|
||||
displayName: 'Run mypy'
|
159
azure-pipelines-release.yml
Normal file
159
azure-pipelines-release.yml
Normal file
@@ -0,0 +1,159 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '5.2'
|
||||
- group: docker
|
||||
- group: github
|
||||
- group: twine
|
||||
|
||||
|
||||
stages:
|
||||
|
||||
- stage: 'Validate'
|
||||
jobs:
|
||||
- job: 'VersionValidate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: |
|
||||
setup_version="$(python setup.py -V)"
|
||||
branch_version="$(Build.SourceBranchName)"
|
||||
|
||||
if [ "${setup_version}" != "${branch_version}" ]; then
|
||||
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
|
||||
exit 1
|
||||
fi
|
||||
displayName: 'Check version of branch/tag'
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
jq curl
|
||||
|
||||
release="$(Build.SourceBranchName)"
|
||||
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
|
||||
|
||||
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${created_by} is not allowed to create an release!"
|
||||
exit 1
|
||||
displayName: 'Check rights'
|
||||
|
||||
- stage: 'Build'
|
||||
jobs:
|
||||
- job: 'ReleasePython'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
displayName: 'Use Python 3.7'
|
||||
inputs:
|
||||
versionSpec: '3.7'
|
||||
- script: pip install twine wheel
|
||||
displayName: 'Install tools'
|
||||
- script: python setup.py sdist bdist_wheel
|
||||
displayName: 'Build package'
|
||||
- script: |
|
||||
export TWINE_USERNAME="$(twineUser)"
|
||||
export TWINE_PASSWORD="$(twinePassword)"
|
||||
|
||||
twine upload dist/* --skip-existing
|
||||
displayName: 'Upload pypi'
|
||||
- job: 'ReleaseDocker'
|
||||
timeoutInMinutes: 240
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 5
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
buildMachine: 'qemux86-64,intel-nuc'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
buildMachine: 'qemux86'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
buildMachine: 'qemuarm,raspberrypi'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
buildMachine: 'raspberrypi2,raspberrypi3,raspberrypi4,odroid-xu,tinker'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime'
|
||||
steps:
|
||||
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
|
||||
displayName: 'Docker hub login'
|
||||
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
|
||||
displayName: 'Install Builder'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t generic --docker-hub homeassistant
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t machine --docker-hub homeassistant
|
||||
displayName: 'Build Release'
|
||||
|
||||
- stage: 'Publish'
|
||||
jobs:
|
||||
- job: 'ReleaseHassio'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git jq curl
|
||||
|
||||
git config --global user.name "Pascal Vizeli"
|
||||
git config --global user.email "pvizeli@syshack.ch"
|
||||
git config --global credential.helper store
|
||||
|
||||
echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials
|
||||
displayName: 'Install requirements'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
version="$(Build.SourceBranchName)"
|
||||
|
||||
git clone https://github.com/home-assistant/hassio-version
|
||||
cd hassio-version
|
||||
|
||||
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
|
||||
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
|
||||
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
|
||||
|
||||
if [[ "$version" =~ b ]]; then
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
else
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
sed -i "s|$stable_version|$version|g" stable.json
|
||||
fi
|
||||
|
||||
git commit -am "Bump Home Assistant $version"
|
||||
git push
|
||||
displayName: 'Update version files'
|
99
azure-pipelines-wheels.yml
Normal file
99
azure-pipelines-wheels.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
paths:
|
||||
include:
|
||||
- requirements_all.txt
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '1.0-3.7-alpine3.10'
|
||||
- group: wheels
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Wheels'
|
||||
timeoutInMinutes: 360
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 5
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
binfmt-support \
|
||||
curl
|
||||
|
||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
||||
sudo update-binfmts --enable qemu-arm
|
||||
sudo update-binfmts --enable qemu-aarch64
|
||||
displayName: 'Initial cross build'
|
||||
- script: |
|
||||
mkdir -p .ssh
|
||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
||||
chmod 600 .ssh/*
|
||||
displayName: 'Install ssh key'
|
||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
||||
displayName: 'Install wheels builder'
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_wheels.txt
|
||||
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
|
||||
touch requirements_diff.txt
|
||||
else
|
||||
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt
|
||||
fi
|
||||
|
||||
requirement_files="requirements_wheels.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# pytradfri|pytradfri|g" ${requirement_file}
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file}
|
||||
sed -i "s|# raspihats|raspihats|g" ${requirement_file}
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file}
|
||||
sed -i "s|# blinkt|blinkt|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file}
|
||||
sed -i "s|# i2csense|i2csense|g" ${requirement_file}
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||
sed -i "s|# decora|decora|g" ${requirement_file}
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
|
||||
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
|
||||
done
|
||||
displayName: 'Prepare requirements files for Hass.io'
|
||||
- script: |
|
||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
||||
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
|
||||
--index $(wheelsIndex) \
|
||||
--requirement requirements_wheels.txt \
|
||||
--requirement-diff requirements_diff.txt \
|
||||
--upload rsync \
|
||||
--remote wheels@$(wheelsHost):/opt/wheels
|
||||
displayName: 'Run wheels build'
|
@@ -1,186 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '3.2'
|
||||
- name: versionWheels
|
||||
value: '0.3'
|
||||
- group: docker
|
||||
- group: wheels
|
||||
- group: github
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Wheels'
|
||||
condition: eq(variables['Build.SourceBranchName'], 'dev')
|
||||
timeoutInMinutes: 360
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
binfmt-support
|
||||
|
||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
||||
sudo update-binfmts --enable qemu-arm
|
||||
sudo update-binfmts --enable qemu-aarch64
|
||||
displayName: 'Initial cross build'
|
||||
- script: |
|
||||
mkdir -p .ssh
|
||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
||||
chmod 600 .ssh/*
|
||||
displayName: 'Install ssh key'
|
||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
||||
displayName: 'Install wheels builder'
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_hassio.txt
|
||||
|
||||
# Enable because we can build it
|
||||
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
|
||||
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
|
||||
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
|
||||
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
|
||||
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
|
||||
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
|
||||
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
|
||||
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
|
||||
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
|
||||
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
|
||||
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
|
||||
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
|
||||
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
|
||||
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
|
||||
sed -i "s|# decora|decora|g" requirements_hassio.txt
|
||||
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
|
||||
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
|
||||
|
||||
# Disable because of error
|
||||
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
|
||||
displayName: 'Prepare requirements files for Hass.io'
|
||||
- script: |
|
||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
||||
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
|
||||
--index https://wheels.hass.io \
|
||||
--requirement requirements_hassio.txt \
|
||||
--upload rsync \
|
||||
--remote wheels@$(wheelsHost):/opt/wheels
|
||||
displayName: 'Run wheels build'
|
||||
|
||||
|
||||
- job: 'Release'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
timeoutInMinutes: 120
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
strategy:
|
||||
maxParallel: 5
|
||||
matrix:
|
||||
amd64:
|
||||
buildArch: 'amd64'
|
||||
buildMachine: 'qemux86-64,intel-nuc'
|
||||
i386:
|
||||
buildArch: 'i386'
|
||||
buildMachine: 'qemux86'
|
||||
armhf:
|
||||
buildArch: 'armhf'
|
||||
buildMachine: 'qemuarm,raspberrypi'
|
||||
armv7:
|
||||
buildArch: 'armv7'
|
||||
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
|
||||
aarch64:
|
||||
buildArch: 'aarch64'
|
||||
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
|
||||
steps:
|
||||
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
|
||||
displayName: 'Docker hub login'
|
||||
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
|
||||
displayName: 'Install Builder'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t generic --docker-hub homeassistant
|
||||
|
||||
sudo docker run --rm --privileged \
|
||||
-v ~/.docker:/root/.docker \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
homeassistant/amd64-builder:$(versionBuilder) \
|
||||
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
|
||||
-r https://github.com/home-assistant/hassio-homeassistant \
|
||||
-t machine --docker-hub homeassistant
|
||||
displayName: 'Build Release'
|
||||
|
||||
|
||||
- job: 'ReleasePublish'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release'))
|
||||
dependsOn:
|
||||
- 'Release'
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git jq
|
||||
|
||||
git config --global user.name "Pascal Vizeli"
|
||||
git config --global user.email "pvizeli@syshack.ch"
|
||||
git config --global credential.helper store
|
||||
|
||||
echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME\.git-credentials
|
||||
displayName: 'Install requirements'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
version="$(Build.SourceBranchName)"
|
||||
|
||||
git clone https://github.com/home-assistant/hassio-version
|
||||
cd hassio-version
|
||||
|
||||
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
|
||||
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
|
||||
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
|
||||
|
||||
if [[ "$version" =~ b ]]; then
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
else
|
||||
sed -i "s|$dev_version|$version|g" dev.json
|
||||
sed -i "s|$beta_version|$version|g" beta.json
|
||||
sed -i "s|$stable_version|$version|g" stable.json
|
||||
fi
|
||||
|
||||
git commit -am "Bump Home Assistant $version"
|
||||
git push
|
@@ -547,7 +547,7 @@ class AuthStore:
|
||||
|
||||
def _set_defaults(self) -> None:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
self._users = OrderedDict()
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
admin_group = _system_admin_group()
|
||||
|
@@ -17,7 +17,6 @@ from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,49 +93,11 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
# TEMP: warn users for invalid slugs
|
||||
# Remove after 0.94 or 1.0
|
||||
if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:
|
||||
msg = []
|
||||
|
||||
if cv.INVALID_ENTITY_IDS_FOUND:
|
||||
msg.append(
|
||||
"Your configuration contains invalid entity ID references. "
|
||||
"Please find and update the following. "
|
||||
"This will become a breaking change."
|
||||
)
|
||||
msg.append('\n'.join('- {} -> {}'.format(*item)
|
||||
for item
|
||||
in cv.INVALID_ENTITY_IDS_FOUND.items()))
|
||||
|
||||
if cv.INVALID_SLUGS_FOUND:
|
||||
msg.append(
|
||||
"Your configuration contains invalid slugs. "
|
||||
"Please find and update the following. "
|
||||
"This will become a breaking change."
|
||||
)
|
||||
msg.append('\n'.join('- {} -> {}'.format(*item)
|
||||
for item in cv.INVALID_SLUGS_FOUND.items()))
|
||||
|
||||
if sys.version_info[:3] < (3, 6, 0):
|
||||
hass.components.persistent_notification.async_create(
|
||||
'\n\n'.join(msg), "Config Warning", "config_warning"
|
||||
)
|
||||
|
||||
# TEMP: warn users of invalid extra keys
|
||||
# Remove after 0.92
|
||||
if cv.INVALID_EXTRA_KEYS_FOUND:
|
||||
msg = []
|
||||
msg.append(
|
||||
"Your configuration contains extra keys "
|
||||
"that the platform does not support (but were silently "
|
||||
"accepted before 0.88). Please find and remove the following."
|
||||
"This will become a breaking change."
|
||||
)
|
||||
msg.append('\n'.join('- {}'.format(it)
|
||||
for it in cv.INVALID_EXTRA_KEYS_FOUND))
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
'\n\n'.join(msg), "Config Warning", "config_warning"
|
||||
"Python 3.5 support is deprecated and will "
|
||||
"be removed in the first release after August 1. Please "
|
||||
"upgrade Python.", "Python version", "python_version"
|
||||
)
|
||||
|
||||
return hass
|
||||
|
@@ -36,7 +36,7 @@ def is_on(hass, entity_id=None):
|
||||
continue
|
||||
|
||||
if not hasattr(component, 'is_on'):
|
||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||
_LOGGER.warning("Integration %s has no is_on method.", domain)
|
||||
continue
|
||||
|
||||
if component.is_on(ent_id):
|
||||
|
30
homeassistant/components/adguard/.translations/ca.json
Normal file
30
homeassistant/components/adguard/.translations/ca.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.",
|
||||
"single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "No s'ha pogut connectar."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?",
|
||||
"title": "AdGuard Home (complement de Hass.io)"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Amfitri\u00f3",
|
||||
"password": "Contrasenya",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home utilitza un certificat SSL",
|
||||
"username": "Nom d'usuari",
|
||||
"verify_ssl": "AdGuard Home utilitza un certificat adequat"
|
||||
},
|
||||
"description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.",
|
||||
"title": "Enlla\u00e7ar AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
29
homeassistant/components/adguard/.translations/de.json
Normal file
29
homeassistant/components/adguard/.translations/de.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Fehler beim Herstellen einer Verbindung."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?",
|
||||
"title": "AdGuard Home \u00fcber das Hass.io Add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Passwort",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat",
|
||||
"username": "Benutzername",
|
||||
"verify_ssl": "AdGuard Home verwendet ein richtiges Zertifikat"
|
||||
},
|
||||
"description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern.",
|
||||
"title": "Verkn\u00fcpfe AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/en.json
Normal file
30
homeassistant/components/adguard/.translations/en.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Updated existing configuration.",
|
||||
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home uses a SSL certificate",
|
||||
"username": "Username",
|
||||
"verify_ssl": "AdGuard Home uses a proper certificate"
|
||||
},
|
||||
"description": "Set up your AdGuard Home instance to allow monitoring and control.",
|
||||
"title": "Link your AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
27
homeassistant/components/adguard/.translations/es-419.json
Normal file
27
homeassistant/components/adguard/.translations/es-419.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Error al conectar."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Hass.io: {addon}?",
|
||||
"title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Contrase\u00f1a",
|
||||
"port": "Puerto",
|
||||
"ssl": "AdGuard Home utiliza un certificado SSL",
|
||||
"username": "Nombre de usuario",
|
||||
"verify_ssl": "AdGuard Home utiliza un certificado adecuado"
|
||||
},
|
||||
"description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
21
homeassistant/components/adguard/.translations/it.json
Normal file
21
homeassistant/components/adguard/.translations/it.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Impossibile connettersi."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Porta",
|
||||
"ssl": "AdGuard Home utilizza un certificato SSL",
|
||||
"username": "Nome utente"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/ko.json
Normal file
30
homeassistant/components/adguard/.translations/ko.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
|
||||
"title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"port": "\ud3ec\ud2b8",
|
||||
"ssl": "AdGuard Home \uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4",
|
||||
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984",
|
||||
"verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
|
||||
"title": "AdGuard Home \uc5f0\uacb0"
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/lb.json
Normal file
30
homeassistant/components/adguard/.translations/lb.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.",
|
||||
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Feeler beim verbannen."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Apparat",
|
||||
"password": "Passwuert",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home benotzt een SSL Zertifikat",
|
||||
"username": "Benotzernumm",
|
||||
"verify_ssl": "AdGuard Home benotzt een eegenen Zertifikat"
|
||||
},
|
||||
"description": "Konfigur\u00e9iert \u00e4r AdGuard Home Instanz fir d'Iwwerwaachung an d'Kontroll z'erlaben.",
|
||||
"title": "Verbannt \u00e4ren AdGuard Home"
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/nl.json
Normal file
30
homeassistant/components/adguard/.translations/nl.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Bestaande configuratie bijgewerkt.",
|
||||
"single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Kon niet verbinden."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Hass.io-add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Wachtwoord",
|
||||
"port": "Poort",
|
||||
"ssl": "AdGuard Home maakt gebruik van een SSL certificaat",
|
||||
"username": "Gebruikersnaam",
|
||||
"verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat"
|
||||
},
|
||||
"description": "Stel uw AdGuard Home-instantie in om toezicht en controle mogelijk te maken.",
|
||||
"title": "Link uw AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/no.json
Normal file
30
homeassistant/components/adguard/.translations/no.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Oppdatert eksisterende konfigurasjon.",
|
||||
"single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Tilkobling mislyktes."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Hjem gitt av hass.io tillegget {addon}?",
|
||||
"title": "AdGuard Hjem via Hass.io tillegg"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
|
||||
"username": "Brukernavn",
|
||||
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
|
||||
},
|
||||
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.",
|
||||
"title": "Koble til ditt AdGuard Hjem."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Hjem"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/pl.json
Normal file
30
homeassistant/components/adguard/.translations/pl.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.",
|
||||
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Po\u0142\u0105czenie nieudane."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
|
||||
"title": "AdGuard Home przez dodatek Hass.io"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Has\u0142o",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home u\u017cywa certyfikatu SSL",
|
||||
"username": "Nazwa u\u017cytkownika",
|
||||
"verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu."
|
||||
},
|
||||
"description": "Skonfiguruj swoj\u0105 instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i nadz\u00f3r sieci.",
|
||||
"title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home"
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/pt-BR.json
Normal file
30
homeassistant/components/adguard/.translations/pt-BR.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada.",
|
||||
"single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Falhou ao conectar."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?",
|
||||
"title": "AdGuard Home via add-on Hass.io"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"ssl": "O AdGuard Home usa um certificado SSL",
|
||||
"username": "Nome de usu\u00e1rio",
|
||||
"verify_ssl": "O AdGuard Home usa um certificado apropriado"
|
||||
},
|
||||
"description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle.",
|
||||
"title": "Vincule o seu AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/ru.json
Normal file
30
homeassistant/components/adguard/.translations/ru.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.",
|
||||
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
|
||||
"title": "AdGuard Home (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u0425\u043e\u0441\u0442",
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
|
||||
"port": "\u041f\u043e\u0440\u0442",
|
||||
"ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
|
||||
"username": "\u041b\u043e\u0433\u0438\u043d",
|
||||
"verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
|
||||
},
|
||||
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.",
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/sl.json
Normal file
30
homeassistant/components/adguard/.translations/sl.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.",
|
||||
"single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Povezava ni uspela."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja hass.io add-on {addon} ?",
|
||||
"title": "AdGuard Home preko dodatka Hass.io"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Gostitelj",
|
||||
"password": "Geslo",
|
||||
"port": "Vrata",
|
||||
"ssl": "AdGuard Home uporablja SSL certifikat",
|
||||
"username": "Uporabni\u0161ko ime",
|
||||
"verify_ssl": "AdGuard Home uporablja ustrezen certifikat"
|
||||
},
|
||||
"description": "Nastavite primerek AdGuard Home, da omogo\u010dite spremljanje in nadzor.",
|
||||
"title": "Pove\u017eite svoj AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
29
homeassistant/components/adguard/.translations/sv.json
Normal file
29
homeassistant/components/adguard/.translations/sv.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Det gick inte att ansluta."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io-till\u00e4gget"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "V\u00e4rd",
|
||||
"password": "L\u00f6senord",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat",
|
||||
"username": "Anv\u00e4ndarnamn",
|
||||
"verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat"
|
||||
},
|
||||
"description": "St\u00e4ll in din AdGuard Home-instans f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll.",
|
||||
"title": "L\u00e4nka din AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
14
homeassistant/components/adguard/.translations/vi.json
Normal file
14
homeassistant/components/adguard/.translations/vi.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u0110\u1ecba ch\u1ec9",
|
||||
"password": "M\u1eadt kh\u1ea9u",
|
||||
"port": "C\u1ed5ng",
|
||||
"username": "T\u00ean \u0111\u0103ng nh\u1eadp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
homeassistant/components/adguard/.translations/zh-Hant.json
Normal file
30
homeassistant/components/adguard/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002",
|
||||
"single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002"
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "\u9023\u7dda\u5931\u6557\u3002"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f",
|
||||
"title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 AdGuard Home"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "\u4e3b\u6a5f\u7aef",
|
||||
"password": "\u5bc6\u78bc",
|
||||
"port": "\u901a\u8a0a\u57e0",
|
||||
"ssl": "AdGuard Home \u4f7f\u7528 SSL \u8a8d\u8b49",
|
||||
"username": "\u4f7f\u7528\u8005\u540d\u7a31",
|
||||
"verify_ssl": "AdGuard Home \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
|
||||
},
|
||||
"description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002",
|
||||
"title": "\u9023\u7d50 AdGuard Home\u3002"
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
}
|
||||
}
|
180
homeassistant/components/adguard/__init__.py
Normal file
180
homeassistant/components/adguard/__init__.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Support for AdGuard Home."""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.adguard.const import (
|
||||
CONF_FORCE, DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN,
|
||||
SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, SERVICE_REFRESH,
|
||||
SERVICE_REMOVE_URL)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_URL,
|
||||
CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url})
|
||||
SERVICE_ADD_URL_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url}
|
||||
)
|
||||
SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up the AdGuard Home components."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up AdGuard Home from a config entry."""
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
adguard = AdGuardHome(
|
||||
entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
tls=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
loop=hass.loop,
|
||||
session=session,
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
|
||||
|
||||
for component in 'sensor', 'switch':
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
async def add_url(call) -> None:
|
||||
"""Service call to add a new filter subscription to AdGuard Home."""
|
||||
await adguard.filtering.add_url(
|
||||
call.data.get(CONF_NAME), call.data.get(CONF_URL)
|
||||
)
|
||||
|
||||
async def remove_url(call) -> None:
|
||||
"""Service call to remove a filter subscription from AdGuard Home."""
|
||||
await adguard.filtering.remove_url(call.data.get(CONF_URL))
|
||||
|
||||
async def enable_url(call) -> None:
|
||||
"""Service call to enable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.enable_url(call.data.get(CONF_URL))
|
||||
|
||||
async def disable_url(call) -> None:
|
||||
"""Service call to disable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.disable_url(call.data.get(CONF_URL))
|
||||
|
||||
async def refresh(call) -> None:
|
||||
"""Service call to refresh the filter subscriptions in AdGuard Home."""
|
||||
await adguard.filtering.refresh(call.data.get(CONF_FORCE))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistantType, entry: ConfigType
|
||||
) -> bool:
|
||||
"""Unload AdGuard Home config entry."""
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||
|
||||
for component in 'sensor', 'switch':
|
||||
await hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AdGuardHomeEntity(Entity):
|
||||
"""Defines a base AdGuard Home entity."""
|
||||
|
||||
def __init__(self, adguard, name: str, icon: str) -> None:
|
||||
"""Initialize the AdGuard Home entity."""
|
||||
self._name = name
|
||||
self._icon = icon
|
||||
self._available = True
|
||||
self.adguard = adguard
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the mdi icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
try:
|
||||
await self._adguard_update()
|
||||
self._available = True
|
||||
except AdGuardHomeError:
|
||||
if self._available:
|
||||
_LOGGER.debug(
|
||||
"An error occurred while updating AdGuard Home sensor.",
|
||||
exc_info=True,
|
||||
)
|
||||
self._available = False
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
|
||||
"""Defines a AdGuard Home device entity."""
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this AdGuard Home instance."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(
|
||||
DOMAIN,
|
||||
self.adguard.host,
|
||||
self.adguard.port,
|
||||
self.adguard.base_path,
|
||||
)
|
||||
},
|
||||
'name': 'AdGuard Home',
|
||||
'manufacturer': 'AdGuard Team',
|
||||
'sw_version': self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
|
||||
}
|
168
homeassistant/components/adguard/config_flow.py
Normal file
168
homeassistant/components/adguard/config_flow.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""Config flow to configure the AdGuard Home integration."""
|
||||
import logging
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.adguard.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME,
|
||||
CONF_VERIFY_SSL)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
"""Handle a AdGuard Home config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
_hassio_discovery = None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize AgGuard Home flow."""
|
||||
pass
|
||||
|
||||
async def _show_setup_form(self, errors=None):
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=3000): vol.Coerce(int),
|
||||
vol.Optional(CONF_USERNAME): str,
|
||||
vol.Optional(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_SSL, default=True): bool,
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def _show_hassio_form(self, errors=None):
|
||||
"""Show the Hass.io confirmation form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id='hassio_confirm',
|
||||
description_placeholders={
|
||||
'addon': self._hassio_discovery['addon']
|
||||
},
|
||||
data_schema=vol.Schema({}),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason='single_instance_allowed')
|
||||
|
||||
if user_input is None:
|
||||
return await self._show_setup_form(user_input)
|
||||
|
||||
errors = {}
|
||||
|
||||
session = async_get_clientsession(
|
||||
self.hass, user_input[CONF_VERIFY_SSL]
|
||||
)
|
||||
|
||||
adguard = AdGuardHome(
|
||||
user_input[CONF_HOST],
|
||||
port=user_input[CONF_PORT],
|
||||
username=user_input.get(CONF_USERNAME),
|
||||
password=user_input.get(CONF_PASSWORD),
|
||||
tls=user_input[CONF_SSL],
|
||||
verify_ssl=user_input[CONF_VERIFY_SSL],
|
||||
loop=self.hass.loop,
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
await adguard.version()
|
||||
except AdGuardHomeConnectionError:
|
||||
errors['base'] = 'connection_error'
|
||||
return await self._show_setup_form(errors)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_HOST],
|
||||
data={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_SSL: user_input[CONF_SSL],
|
||||
CONF_USERNAME: user_input.get(CONF_USERNAME),
|
||||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_hassio(self, user_input=None):
|
||||
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
entries = self._async_current_entries()
|
||||
|
||||
if not entries:
|
||||
self._hassio_discovery = user_input
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
cur_entry = entries[0]
|
||||
|
||||
if (cur_entry.data[CONF_HOST] == user_input[CONF_HOST] and
|
||||
cur_entry.data[CONF_PORT] == user_input[CONF_PORT]):
|
||||
return self.async_abort(reason='single_instance_allowed')
|
||||
|
||||
is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED
|
||||
|
||||
if is_loaded:
|
||||
await self.hass.config_entries.async_unload(cur_entry.entry_id)
|
||||
|
||||
self.hass.config_entries.async_update_entry(cur_entry, data={
|
||||
**cur_entry.data,
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
})
|
||||
|
||||
if is_loaded:
|
||||
await self.hass.config_entries.async_setup(cur_entry.entry_id)
|
||||
|
||||
return self.async_abort(reason='existing_instance_updated')
|
||||
|
||||
async def async_step_hassio_confirm(self, user_input=None):
|
||||
"""Confirm Hass.io discovery."""
|
||||
if user_input is None:
|
||||
return await self._show_hassio_form()
|
||||
|
||||
errors = {}
|
||||
|
||||
session = async_get_clientsession(self.hass, False)
|
||||
|
||||
adguard = AdGuardHome(
|
||||
self._hassio_discovery[CONF_HOST],
|
||||
port=self._hassio_discovery[CONF_PORT],
|
||||
tls=False,
|
||||
loop=self.hass.loop,
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
await adguard.version()
|
||||
except AdGuardHomeConnectionError:
|
||||
errors['base'] = 'connection_error'
|
||||
return await self._show_hassio_form(errors)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self._hassio_discovery['addon'],
|
||||
data={
|
||||
CONF_HOST: self._hassio_discovery[CONF_HOST],
|
||||
CONF_PORT: self._hassio_discovery[CONF_PORT],
|
||||
CONF_PASSWORD: None,
|
||||
CONF_SSL: False,
|
||||
CONF_USERNAME: None,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
)
|
14
homeassistant/components/adguard/const.py
Normal file
14
homeassistant/components/adguard/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants for the AdGuard Home integration."""
|
||||
|
||||
DOMAIN = 'adguard'
|
||||
|
||||
DATA_ADGUARD_CLIENT = 'adguard_client'
|
||||
DATA_ADGUARD_VERION = 'adguard_version'
|
||||
|
||||
CONF_FORCE = 'force'
|
||||
|
||||
SERVICE_ADD_URL = 'add_url'
|
||||
SERVICE_DISABLE_URL = 'disable_url'
|
||||
SERVICE_ENABLE_URL = 'enable_url'
|
||||
SERVICE_REFRESH = 'refresh'
|
||||
SERVICE_REMOVE_URL = 'remove_url'
|
13
homeassistant/components/adguard/manifest.json
Normal file
13
homeassistant/components/adguard/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "adguard",
|
||||
"name": "AdGuard Home",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/adguard",
|
||||
"requirements": [
|
||||
"adguardhome==0.2.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@frenck"
|
||||
]
|
||||
}
|
232
homeassistant/components/adguard/sensor.py
Normal file
232
homeassistant/components/adguard/sensor.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""Support for AdGuard Home sensors."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from adguardhome import AdGuardHomeConnectionError
|
||||
|
||||
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
|
||||
from homeassistant.components.adguard.const import (
|
||||
DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
PARALLEL_UPDATES = 4
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up AdGuard Home sensor based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
|
||||
|
||||
try:
|
||||
version = await adguard.version()
|
||||
except AdGuardHomeConnectionError as exception:
|
||||
raise PlatformNotReady from exception
|
||||
|
||||
hass.data[DOMAIN][DATA_ADGUARD_VERION] = version
|
||||
|
||||
sensors = [
|
||||
AdGuardHomeDNSQueriesSensor(adguard),
|
||||
AdGuardHomeBlockedFilteringSensor(adguard),
|
||||
AdGuardHomePercentageBlockedSensor(adguard),
|
||||
AdGuardHomeReplacedParentalSensor(adguard),
|
||||
AdGuardHomeReplacedSafeBrowsingSensor(adguard),
|
||||
AdGuardHomeReplacedSafeSearchSensor(adguard),
|
||||
AdGuardHomeAverageProcessingTimeSensor(adguard),
|
||||
AdGuardHomeRulesCountSensor(adguard),
|
||||
]
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
|
||||
"""Defines a AdGuard Home sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adguard,
|
||||
name: str,
|
||||
icon: str,
|
||||
measurement: str,
|
||||
unit_of_measurement: str,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
self._state = None
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self.measurement = measurement
|
||||
|
||||
super().__init__(adguard, name, icon)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return '_'.join(
|
||||
[
|
||||
DOMAIN,
|
||||
self.adguard.host,
|
||||
str(self.adguard.port),
|
||||
'sensor',
|
||||
self.measurement,
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
||||
class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home DNS Queries sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard DNS Queries',
|
||||
'mdi:magnify',
|
||||
'dns_queries',
|
||||
'queries',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.stats.dns_queries()
|
||||
|
||||
|
||||
class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home blocked by filtering sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard DNS Queries Blocked',
|
||||
'mdi:magnify-close',
|
||||
'blocked_filtering',
|
||||
'queries',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.stats.blocked_filtering()
|
||||
|
||||
|
||||
class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home blocked percentage sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard DNS Queries Blocked Ratio',
|
||||
'mdi:magnify-close',
|
||||
'blocked_percentage',
|
||||
'%',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
percentage = await self.adguard.stats.blocked_percentage()
|
||||
self._state = "{:.2f}".format(percentage)
|
||||
|
||||
|
||||
class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by parental control sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard Parental Control Blocked',
|
||||
'mdi:human-male-girl',
|
||||
'blocked_parental',
|
||||
'requests',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.stats.replaced_parental()
|
||||
|
||||
|
||||
class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by safe browsing sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard Safe Browsing Blocked',
|
||||
'mdi:shield-half-full',
|
||||
'blocked_safebrowsing',
|
||||
'requests',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.stats.replaced_safebrowsing()
|
||||
|
||||
|
||||
class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by safe search sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'Searches Safe Search Enforced',
|
||||
'mdi:shield-search',
|
||||
'enforced_safesearch',
|
||||
'requests',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.stats.replaced_safesearch()
|
||||
|
||||
|
||||
class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home average processing time sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard Average Processing Speed',
|
||||
'mdi:speedometer',
|
||||
'average_speed',
|
||||
'ms',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
average = await self.adguard.stats.avg_processing_time()
|
||||
self._state = "{:.2f}".format(average)
|
||||
|
||||
|
||||
class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home rules count sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
'AdGuard Rules Count',
|
||||
'mdi:counter',
|
||||
'rules_count',
|
||||
'rules',
|
||||
)
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.filtering.rules_count()
|
37
homeassistant/components/adguard/services.yaml
Normal file
37
homeassistant/components/adguard/services.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
add_url:
|
||||
description: Add a new filter subscription to AdGuard Home.
|
||||
fields:
|
||||
name:
|
||||
description: The name of the filter subscription.
|
||||
example: Example
|
||||
url:
|
||||
description: The filter URL to subscribe to, containing the filter rules.
|
||||
example: https://www.example.com/filter/1.txt
|
||||
|
||||
remove_url:
|
||||
description: Removes a filter subscription from AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
description: The filter subscription URL to remove.
|
||||
example: https://www.example.com/filter/1.txt
|
||||
|
||||
enable_url:
|
||||
description: Enables a filter subscription in AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
description: The filter subscription URL to enable.
|
||||
example: https://www.example.com/filter/1.txt
|
||||
|
||||
disable_url:
|
||||
description: Disables a filter subscription in AdGuard Home.
|
||||
fields:
|
||||
url:
|
||||
description: The filter subscription URL to disable.
|
||||
example: https://www.example.com/filter/1.txt
|
||||
|
||||
refresh:
|
||||
description: Refresh all filter subscriptions in AdGuard Home.
|
||||
fields:
|
||||
force:
|
||||
description: Force update (by passes AdGuard Home throttling).
|
||||
example: '"true" to force, "false" or omit for a regular refresh.'
|
30
homeassistant/components/adguard/strings.json
Normal file
30
homeassistant/components/adguard/strings.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "AdGuard Home",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your AdGuard Home.",
|
||||
"description": "Set up your AdGuard Home instance to allow monitoring and control.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"ssl": "AdGuard Home uses a SSL certificate",
|
||||
"verify_ssl": "AdGuard Home uses a proper certificate"
|
||||
}
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "AdGuard Home via Hass.io add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.",
|
||||
"existing_instance_updated": "Updated existing configuration."
|
||||
}
|
||||
}
|
||||
}
|
233
homeassistant/components/adguard/switch.py
Normal file
233
homeassistant/components/adguard/switch.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""Support for AdGuard Home switches."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError
|
||||
|
||||
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
|
||||
from homeassistant.components.adguard.const import (
|
||||
DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up AdGuard Home switch based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
|
||||
|
||||
try:
|
||||
version = await adguard.version()
|
||||
except AdGuardHomeConnectionError as exception:
|
||||
raise PlatformNotReady from exception
|
||||
|
||||
hass.data[DOMAIN][DATA_ADGUARD_VERION] = version
|
||||
|
||||
switches = [
|
||||
AdGuardHomeProtectionSwitch(adguard),
|
||||
AdGuardHomeFilteringSwitch(adguard),
|
||||
AdGuardHomeParentalSwitch(adguard),
|
||||
AdGuardHomeSafeBrowsingSwitch(adguard),
|
||||
AdGuardHomeSafeSearchSwitch(adguard),
|
||||
AdGuardHomeQueryLogSwitch(adguard),
|
||||
]
|
||||
async_add_entities(switches, True)
|
||||
|
||||
|
||||
class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity):
|
||||
"""Defines a AdGuard Home switch."""
|
||||
|
||||
def __init__(self, adguard, name: str, icon: str, key: str):
|
||||
"""Initialize AdGuard Home switch."""
|
||||
self._state = False
|
||||
self._key = key
|
||||
super().__init__(adguard, name, icon)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return '_'.join(
|
||||
[
|
||||
DOMAIN,
|
||||
self.adguard.host,
|
||||
str(self.adguard.port),
|
||||
'switch',
|
||||
self._key,
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self._state
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the switch."""
|
||||
try:
|
||||
await self._adguard_turn_off()
|
||||
except AdGuardHomeError:
|
||||
_LOGGER.error(
|
||||
"An error occurred while turning off AdGuard Home switch."
|
||||
)
|
||||
self._available = False
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn on the switch."""
|
||||
try:
|
||||
await self._adguard_turn_on()
|
||||
except AdGuardHomeError:
|
||||
_LOGGER.error(
|
||||
"An error occurred while turning on AdGuard Home switch."
|
||||
)
|
||||
self._available = False
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home protection switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Protection", 'mdi:shield-check', 'protection'
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.disable_protection()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.enable_protection()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.protection_enabled()
|
||||
|
||||
|
||||
class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home parental control switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Parental Control", 'mdi:shield-check', 'parental'
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.parental.disable()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.parental.enable()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.parental.enabled()
|
||||
|
||||
|
||||
class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home safe search switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Safe Search", 'mdi:shield-check', 'safesearch'
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.safesearch.disable()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.safesearch.enable()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.safesearch.enabled()
|
||||
|
||||
|
||||
class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home safe search switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
"AdGuard Safe Browsing",
|
||||
'mdi:shield-check',
|
||||
'safebrowsing',
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.safebrowsing.disable()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.safebrowsing.enable()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.safebrowsing.enabled()
|
||||
|
||||
|
||||
class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home filtering switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Filtering", 'mdi:shield-check', 'filtering'
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.filtering.disable()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.filtering.enable()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.filtering.enabled()
|
||||
|
||||
|
||||
class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home query log switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Query Log", 'mdi:shield-check', 'querylog'
|
||||
)
|
||||
|
||||
async def _adguard_turn_off(self) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.adguard.querylog.disable()
|
||||
|
||||
async def _adguard_turn_on(self) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.adguard.querylog.enable()
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.querylog.enabled()
|
@@ -177,6 +177,11 @@ class AfterShipSensor(Entity):
|
||||
if track['title'] is None
|
||||
else track['title']
|
||||
)
|
||||
last_checkpoint = (
|
||||
"Shipment pending"
|
||||
if track['tag'] == "Pending"
|
||||
else track['checkpoints'][-1]
|
||||
)
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
trackings.append({
|
||||
'name': name,
|
||||
@@ -187,7 +192,7 @@ class AfterShipSensor(Entity):
|
||||
'last_update': track['updated_at'],
|
||||
'expected_delivery': track['expected_delivery'],
|
||||
'status': track['tag'],
|
||||
'last_checkpoint': track['checkpoints'][-1]
|
||||
'last_checkpoint': last_checkpoint
|
||||
})
|
||||
|
||||
if status not in status_to_ignore:
|
||||
|
@@ -19,6 +19,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
FORMAT_TEXT = 'text'
|
||||
FORMAT_NUMBER = 'number'
|
||||
ATTR_CODE_ARM_REQUIRED = 'code_arm_required'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
@@ -87,6 +88,11 @@ class AlarmControlPanel(Entity):
|
||||
"""Last change triggered by."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def code_arm_required(self):
|
||||
"""Whether the code is required for arm actions."""
|
||||
return True
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
@@ -159,6 +165,7 @@ class AlarmControlPanel(Entity):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
ATTR_CHANGED_BY: self.changed_by
|
||||
ATTR_CHANGED_BY: self.changed_by,
|
||||
ATTR_CODE_ARM_REQUIRED: self.code_arm_required
|
||||
}
|
||||
return state_attr
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Support for repeating alerts when conditions are met."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -117,7 +118,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in entities]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
@@ -222,7 +223,7 @@ class Alert(ToggleEntity):
|
||||
async def _schedule_notify(self):
|
||||
"""Schedule a notification."""
|
||||
delay = self._delay[self._next_delay]
|
||||
next_msg = datetime.now() + delay
|
||||
next_msg = now() + delay
|
||||
self._cancel = \
|
||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||
|
@@ -4,5 +4,8 @@
|
||||
"documentation": "https://www.home-assistant.io/components/alert",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"after_dependencies": [
|
||||
"notify"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -5,12 +5,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
from . import flash_briefings, intent, smart_home
|
||||
from . import flash_briefings, intent, smart_home_http
|
||||
from .const import (
|
||||
CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL,
|
||||
CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER,
|
||||
CONF_ENTITY_CONFIG)
|
||||
CONF_ENTITY_CONFIG, CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,9 +19,9 @@ CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_SMART_HOME = 'smart_home'
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(smart_home.CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema({
|
||||
@@ -65,6 +66,6 @@ async def async_setup(hass, config):
|
||||
pass
|
||||
else:
|
||||
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
|
||||
await smart_home.async_setup(hass, smart_home_config)
|
||||
await smart_home_http.async_setup(hass, smart_home_config)
|
||||
|
||||
return True
|
||||
|
@@ -9,7 +9,6 @@ import async_timeout
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util import dt
|
||||
from .const import DEFAULT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,7 +38,7 @@ class Auth:
|
||||
self._prefs = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock(loop=hass.loop)
|
||||
self._get_token_lock = asyncio.Lock()
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
@@ -97,7 +96,7 @@ class Auth:
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
response = await session.post(LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
data=lwa_params,
|
||||
|
604
homeassistant/components/alexa/capabilities.py
Normal file
604
homeassistant/components/alexa/capabilities.py
Normal file
@@ -0,0 +1,604 @@
|
||||
"""Alexa capabilities."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_LOCKED,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNLOCKED,
|
||||
)
|
||||
import homeassistant.components.climate.const as climate
|
||||
from homeassistant.components import (
|
||||
light,
|
||||
fan,
|
||||
cover,
|
||||
)
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlexaCapibility:
|
||||
"""Base class for Alexa capability interfaces.
|
||||
|
||||
The Smart Home Skills API defines a number of "capability interfaces",
|
||||
roughly analogous to domains in Home Assistant. The supported interfaces
|
||||
describe what actions can be performed on a particular device.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/message-guide.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize an Alexa capibility."""
|
||||
self.entity = entity
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def properties_supported():
|
||||
"""Return what properties this entity supports."""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def properties_proactively_reported():
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def properties_retrievable():
|
||||
"""Return True if properties can be retrieved."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_property(name):
|
||||
"""Read and return a property.
|
||||
|
||||
Return value should be a dict, or raise UnsupportedProperty.
|
||||
|
||||
Properties can also have a timeOfSample and uncertaintyInMilliseconds,
|
||||
but returning those metadata is not yet implemented.
|
||||
"""
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
@staticmethod
|
||||
def supports_deactivation():
|
||||
"""Applicable only to scenes."""
|
||||
return None
|
||||
|
||||
def serialize_discovery(self):
|
||||
"""Serialize according to the Discovery API."""
|
||||
result = {
|
||||
'type': 'AlexaInterface',
|
||||
'interface': self.name(),
|
||||
'version': '3',
|
||||
'properties': {
|
||||
'supported': self.properties_supported(),
|
||||
'proactivelyReported': self.properties_proactively_reported(),
|
||||
'retrievable': self.properties_retrievable(),
|
||||
},
|
||||
}
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
supports_deactivation = self.supports_deactivation()
|
||||
if supports_deactivation is not None:
|
||||
result['supportsDeactivation'] = supports_deactivation
|
||||
return result
|
||||
|
||||
def serialize_properties(self):
|
||||
"""Return properties serialized for an API response."""
|
||||
for prop in self.properties_supported():
|
||||
prop_name = prop['name']
|
||||
# pylint: disable=assignment-from-no-return
|
||||
prop_value = self.get_property(prop_name)
|
||||
if prop_value is not None:
|
||||
yield {
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': prop_value,
|
||||
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
|
||||
'uncertaintyInMilliseconds': 0
|
||||
}
|
||||
|
||||
|
||||
class AlexaEndpointHealth(AlexaCapibility):
|
||||
"""Implements Alexa.EndpointHealth.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.EndpointHealth'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'connectivity'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return False
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'connectivity':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_UNAVAILABLE:
|
||||
return {'value': 'UNREACHABLE'}
|
||||
return {'value': 'OK'}
|
||||
|
||||
|
||||
class AlexaPowerController(AlexaCapibility):
|
||||
"""Implements Alexa.PowerController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.PowerController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'powerState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'powerState':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
||||
|
||||
else:
|
||||
is_on = self.entity.state != STATE_OFF
|
||||
|
||||
return 'ON' if is_on else 'OFF'
|
||||
|
||||
|
||||
class AlexaLockController(AlexaCapibility):
|
||||
"""Implements Alexa.LockController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.LockController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'lockState'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'lockState':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_LOCKED:
|
||||
return 'LOCKED'
|
||||
if self.entity.state == STATE_UNLOCKED:
|
||||
return 'UNLOCKED'
|
||||
return 'JAMMED'
|
||||
|
||||
|
||||
class AlexaSceneController(AlexaCapibility):
|
||||
"""Implements Alexa.SceneController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html
|
||||
"""
|
||||
|
||||
def __init__(self, entity, supports_deactivation):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.supports_deactivation = lambda: supports_deactivation
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.SceneController'
|
||||
|
||||
|
||||
class AlexaBrightnessController(AlexaCapibility):
|
||||
"""Implements Alexa.BrightnessController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.BrightnessController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'brightness'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'brightness':
|
||||
raise UnsupportedProperty(name)
|
||||
if 'brightness' in self.entity.attributes:
|
||||
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
||||
return 0
|
||||
|
||||
|
||||
class AlexaColorController(AlexaCapibility):
|
||||
"""Implements Alexa.ColorController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.ColorController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'color'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'color':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
hue, saturation = self.entity.attributes.get(
|
||||
light.ATTR_HS_COLOR, (0, 0))
|
||||
|
||||
return {
|
||||
'hue': hue,
|
||||
'saturation': saturation / 100.0,
|
||||
'brightness': self.entity.attributes.get(
|
||||
light.ATTR_BRIGHTNESS, 0) / 255.0,
|
||||
}
|
||||
|
||||
|
||||
class AlexaColorTemperatureController(AlexaCapibility):
|
||||
"""Implements Alexa.ColorTemperatureController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.ColorTemperatureController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'colorTemperatureInKelvin'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'colorTemperatureInKelvin':
|
||||
raise UnsupportedProperty(name)
|
||||
if 'color_temp' in self.entity.attributes:
|
||||
return color_util.color_temperature_mired_to_kelvin(
|
||||
self.entity.attributes['color_temp'])
|
||||
return 0
|
||||
|
||||
|
||||
class AlexaPercentageController(AlexaCapibility):
|
||||
"""Implements Alexa.PercentageController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.PercentageController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'percentage'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'percentage':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
return PERCENTAGE_FAN_MAP.get(speed, 0)
|
||||
|
||||
if self.entity.domain == cover.DOMAIN:
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION, 0)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class AlexaSpeaker(AlexaCapibility):
|
||||
"""Implements Alexa.Speaker.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.Speaker'
|
||||
|
||||
|
||||
class AlexaStepSpeaker(AlexaCapibility):
|
||||
"""Implements Alexa.StepSpeaker.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.StepSpeaker'
|
||||
|
||||
|
||||
class AlexaPlaybackController(AlexaCapibility):
|
||||
"""Implements Alexa.PlaybackController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.PlaybackController'
|
||||
|
||||
|
||||
class AlexaInputController(AlexaCapibility):
|
||||
"""Implements Alexa.InputController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.InputController'
|
||||
|
||||
|
||||
class AlexaTemperatureSensor(AlexaCapibility):
|
||||
"""Implements Alexa.TemperatureSensor.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.TemperatureSensor'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'temperature'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'temperature':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
temp = self.entity.state
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
temp = self.entity.attributes.get(
|
||||
climate.ATTR_CURRENT_TEMPERATURE)
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
||||
|
||||
|
||||
class AlexaContactSensor(AlexaCapibility):
|
||||
"""Implements Alexa.ContactSensor.
|
||||
|
||||
The Alexa.ContactSensor interface describes the properties and events used
|
||||
to report the state of an endpoint that detects contact between two
|
||||
surfaces. For example, a contact sensor can report whether a door or window
|
||||
is open.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.ContactSensor'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'detectionState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'detectionState':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_ON:
|
||||
return 'DETECTED'
|
||||
return 'NOT_DETECTED'
|
||||
|
||||
|
||||
class AlexaMotionSensor(AlexaCapibility):
|
||||
"""Implements Alexa.MotionSensor.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.MotionSensor'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{'name': 'detectionState'}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != 'detectionState':
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_ON:
|
||||
return 'DETECTED'
|
||||
return 'NOT_DETECTED'
|
||||
|
||||
|
||||
class AlexaThermostatController(AlexaCapibility):
|
||||
"""Implements Alexa.ThermostatController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return 'Alexa.ThermostatController'
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
properties = [{'name': 'thermostatMode'}]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||
properties.append({'name': 'targetSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE:
|
||||
properties.append({'name': 'lowerSetpoint'})
|
||||
properties.append({'name': 'upperSetpoint'})
|
||||
return properties
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name == 'thermostatMode':
|
||||
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
|
||||
|
||||
if preset in API_THERMOSTAT_PRESETS:
|
||||
mode = API_THERMOSTAT_PRESETS[preset]
|
||||
else:
|
||||
mode = API_THERMOSTAT_MODES.get(self.entity.state)
|
||||
if mode is None:
|
||||
_LOGGER.error(
|
||||
"%s (%s) has unsupported state value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
self.entity.state)
|
||||
raise UnsupportedProperty(name)
|
||||
return mode
|
||||
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
elif name == 'upperSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
else:
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if temp is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
69
homeassistant/components/alexa/config.py
Normal file
69
homeassistant/components/alexa/config.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Config helpers for Alexa."""
|
||||
from .state_report import async_enable_proactive_mode
|
||||
|
||||
|
||||
class AbstractConfig:
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_unsub_proactive_report = None
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
"""Return if config supports auth."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
"""Return if states should be proactively reported."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
"""Endpoint for report state."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
"""Return entity config."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_reporting_states(self):
|
||||
"""Return if proactive mode is enabled."""
|
||||
return self._unsub_proactive_report is not None
|
||||
|
||||
async def async_enable_proactive_mode(self):
|
||||
"""Enable proactive mode."""
|
||||
if self._unsub_proactive_report is None:
|
||||
self._unsub_proactive_report = self.hass.async_create_task(
|
||||
async_enable_proactive_mode(self.hass, self)
|
||||
)
|
||||
try:
|
||||
await self._unsub_proactive_report
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self._unsub_proactive_report = None
|
||||
raise
|
||||
|
||||
async def async_disable_proactive_mode(self):
|
||||
"""Disable proactive mode."""
|
||||
unsub_func = await self._unsub_proactive_report
|
||||
if unsub_func:
|
||||
unsub_func()
|
||||
self._unsub_proactive_report = None
|
||||
|
||||
def should_expose(self, entity_id):
|
||||
"""If an entity should be exposed."""
|
||||
# pylint: disable=no-self-use
|
||||
return False
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Get an access token."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_accept_grant(self, code):
|
||||
"""Accept a grant."""
|
||||
raise NotImplementedError
|
@@ -1,4 +1,14 @@
|
||||
"""Constants for the Alexa integration."""
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import fan
|
||||
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
|
||||
# Flash briefing constants
|
||||
@@ -25,4 +35,74 @@ SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
API_DIRECTIVE = 'directive'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
API_EVENT = 'event'
|
||||
API_CONTEXT = 'context'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
API_SCOPE = 'scope'
|
||||
API_CHANGE = 'change'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
|
||||
API_TEMP_UNITS = {
|
||||
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
||||
TEMP_CELSIUS: 'CELSIUS',
|
||||
}
|
||||
|
||||
# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a
|
||||
# reverse mapping of this dict and we want to map the first occurrance of OFF
|
||||
# back to HA state.
|
||||
API_THERMOSTAT_MODES = OrderedDict([
|
||||
(climate.HVAC_MODE_HEAT, 'HEAT'),
|
||||
(climate.HVAC_MODE_COOL, 'COOL'),
|
||||
(climate.HVAC_MODE_HEAT_COOL, 'AUTO'),
|
||||
(climate.HVAC_MODE_AUTO, 'AUTO'),
|
||||
(climate.HVAC_MODE_OFF, 'OFF'),
|
||||
(climate.HVAC_MODE_FAN_ONLY, 'OFF'),
|
||||
(climate.HVAC_MODE_DRY, 'OFF'),
|
||||
])
|
||||
API_THERMOSTAT_PRESETS = {
|
||||
climate.PRESET_ECO: 'ECO'
|
||||
}
|
||||
|
||||
PERCENTAGE_FAN_MAP = {
|
||||
fan.SPEED_LOW: 33,
|
||||
fan.SPEED_MEDIUM: 66,
|
||||
fan.SPEED_HIGH: 100,
|
||||
}
|
||||
|
||||
|
||||
class Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
||||
"""
|
||||
|
||||
# Indicates that the event was caused by a customer interaction with an
|
||||
# application. For example, a customer switches on a light, or locks a door
|
||||
# using the Alexa app or an app provided by a device vendor.
|
||||
APP_INTERACTION = 'APP_INTERACTION'
|
||||
|
||||
# Indicates that the event was caused by a physical interaction with an
|
||||
# endpoint. For example manually switching on a light or manually locking a
|
||||
# door lock
|
||||
PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION'
|
||||
|
||||
# Indicates that the event was caused by the periodic poll of an appliance,
|
||||
# which found a change in value. For example, you might poll a temperature
|
||||
# sensor every hour, and send the updated temperature to Alexa.
|
||||
PERIODIC_POLL = 'PERIODIC_POLL'
|
||||
|
||||
# Indicates that the event was caused by the application of a device rule.
|
||||
# For example, a customer configures a rule to switch on a light if a
|
||||
# motion sensor detects motion. In this case, Alexa receives an event from
|
||||
# the motion sensor, and another event from the light to indicate that its
|
||||
# state change was caused by the rule.
|
||||
RULE_TRIGGER = 'RULE_TRIGGER'
|
||||
|
||||
# Indicates that the event was caused by a voice interaction with Alexa.
|
||||
# For example a user speaking to their Echo device.
|
||||
VOICE_INTERACTION = 'VOICE_INTERACTION'
|
||||
|
457
homeassistant/components/alexa/entities.py
Normal file
457
homeassistant/components/alexa/entities.py
Normal file
@@ -0,0 +1,457 @@
|
||||
"""Alexa entity adapters."""
|
||||
from typing import List
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
CONF_NAME,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import (
|
||||
alert, automation, binary_sensor, cover, fan, group,
|
||||
input_boolean, light, lock, media_player, scene, script, sensor, switch)
|
||||
|
||||
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
||||
from .capabilities import (
|
||||
AlexaBrightnessController,
|
||||
AlexaColorController,
|
||||
AlexaColorTemperatureController,
|
||||
AlexaContactSensor,
|
||||
AlexaEndpointHealth,
|
||||
AlexaInputController,
|
||||
AlexaLockController,
|
||||
AlexaMotionSensor,
|
||||
AlexaPercentageController,
|
||||
AlexaPlaybackController,
|
||||
AlexaPowerController,
|
||||
AlexaSceneController,
|
||||
AlexaSpeaker,
|
||||
AlexaStepSpeaker,
|
||||
AlexaTemperatureSensor,
|
||||
AlexaThermostatController,
|
||||
)
|
||||
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
||||
|
||||
class DisplayCategory:
|
||||
"""Possible display categories for Discovery response.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
||||
"""
|
||||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# state change must occur in a specific order. For example, a "watch
|
||||
# Netflix" scene might require the: 1. TV to be powered on & 2. Input set
|
||||
# to HDMI1. Applies to Scenes
|
||||
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||
|
||||
# Indicates media devices with video or photo capabilities.
|
||||
CAMERA = "CAMERA"
|
||||
|
||||
# Indicates an endpoint that detects and reports contact.
|
||||
CONTACT_SENSOR = "CONTACT_SENSOR"
|
||||
|
||||
# Indicates a door.
|
||||
DOOR = "DOOR"
|
||||
|
||||
# Indicates light sources or fixtures.
|
||||
LIGHT = "LIGHT"
|
||||
|
||||
# Indicates an endpoint that detects and reports motion.
|
||||
MOTION_SENSOR = "MOTION_SENSOR"
|
||||
|
||||
# An endpoint that cannot be described in on of the other categories.
|
||||
OTHER = "OTHER"
|
||||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# order of the state change is not important. For example a bedtime scene
|
||||
# might include turning off lights and lowering the thermostat, but the
|
||||
# order is unimportant. Applies to Scenes
|
||||
SCENE_TRIGGER = "SCENE_TRIGGER"
|
||||
|
||||
# Indicates an endpoint that locks.
|
||||
SMARTLOCK = "SMARTLOCK"
|
||||
|
||||
# Indicates modules that are plugged into an existing electrical outlet.
|
||||
# Can control a variety of devices.
|
||||
SMARTPLUG = "SMARTPLUG"
|
||||
|
||||
# Indicates the endpoint is a speaker or speaker system.
|
||||
SPEAKER = "SPEAKER"
|
||||
|
||||
# Indicates in-wall switches wired to the electrical system. Can control a
|
||||
# variety of devices.
|
||||
SWITCH = "SWITCH"
|
||||
|
||||
# Indicates endpoints that report the temperature only.
|
||||
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
||||
|
||||
# Indicates endpoints that control temperature, stand-alone air
|
||||
# conditioners, or heaters with direct temperature control.
|
||||
THERMOSTAT = "THERMOSTAT"
|
||||
|
||||
# Indicates the endpoint is a television.
|
||||
TV = "TV"
|
||||
|
||||
|
||||
class AlexaEntity:
|
||||
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||
|
||||
The API handlers should manipulate entities only through this interface.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, config, entity):
|
||||
"""Initialize Alexa Entity."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.entity = entity
|
||||
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return the Entity ID."""
|
||||
return self.entity.entity_id
|
||||
|
||||
def friendly_name(self):
|
||||
"""Return the Alexa API friendly name."""
|
||||
return self.entity_conf.get(CONF_NAME, self.entity.name)
|
||||
|
||||
def description(self):
|
||||
"""Return the Alexa API description."""
|
||||
return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id)
|
||||
|
||||
def alexa_id(self):
|
||||
"""Return the Alexa API entity id."""
|
||||
return self.entity.entity_id.replace('.', '#')
|
||||
|
||||
def display_categories(self):
|
||||
"""Return a list of display categories."""
|
||||
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
||||
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
||||
return [entity_conf[CONF_DISPLAY_CATEGORIES]]
|
||||
return self.default_display_categories()
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return a list of default display categories.
|
||||
|
||||
This can be overridden by the user in the Home Assistant configuration.
|
||||
|
||||
See also DisplayCategory.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_interface(self, capability):
|
||||
"""Return the given AlexaInterface.
|
||||
|
||||
Raises _UnsupportedInterface.
|
||||
"""
|
||||
pass
|
||||
|
||||
def interfaces(self):
|
||||
"""Return a list of supported interfaces.
|
||||
|
||||
Used for discovery. The list should contain AlexaInterface instances.
|
||||
If the list is empty, this entity will not be discovered.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize_properties(self):
|
||||
"""Yield each supported property in API format."""
|
||||
for interface in self.interfaces():
|
||||
for prop in interface.serialize_properties():
|
||||
yield prop
|
||||
|
||||
def serialize_discovery(self):
|
||||
"""Serialize the entity for discovery."""
|
||||
return {
|
||||
'displayCategories': self.display_categories(),
|
||||
'cookie': {},
|
||||
'endpointId': self.alexa_id(),
|
||||
'friendlyName': self.friendly_name(),
|
||||
'description': self.description(),
|
||||
'manufacturerName': 'Home Assistant',
|
||||
'capabilities': [
|
||||
i.serialize_discovery() for i in self.interfaces()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_entities(hass, config) -> List[AlexaEntity]:
|
||||
"""Return all entities that are supported by Alexa."""
|
||||
entities = []
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
continue
|
||||
|
||||
if state.domain not in ENTITY_ADAPTERS:
|
||||
continue
|
||||
|
||||
alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state)
|
||||
|
||||
if not list(alexa_entity.interfaces()):
|
||||
continue
|
||||
|
||||
entities.append(alexa_entity)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(alert.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(automation.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
||||
class GenericCapabilities(AlexaEntity):
|
||||
"""A generic, on/off device.
|
||||
|
||||
The choice of last resort.
|
||||
"""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
return [AlexaPowerController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(switch.DOMAIN)
|
||||
class SwitchCapabilities(AlexaEntity):
|
||||
"""Class to represent Switch capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.SWITCH]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
return [AlexaPowerController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||
class ClimateCapabilities(AlexaEntity):
|
||||
"""Class to represent Climate capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.THERMOSTAT]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
# If we support two modes, one being off, we allow turning on too.
|
||||
if (climate.HVAC_MODE_OFF in
|
||||
self.entity.attributes[climate.ATTR_HVAC_MODES]):
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
yield AlexaThermostatController(self.hass, self.entity)
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
class CoverCapabilities(AlexaEntity):
|
||||
"""Class to represent Cover capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.DOOR]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & cover.SUPPORT_SET_POSITION:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||
class LightCapabilities(AlexaEntity):
|
||||
"""Class to represent Light capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.LIGHT]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
yield AlexaBrightnessController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR:
|
||||
yield AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield AlexaColorTemperatureController(self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||
class FanCapabilities(AlexaEntity):
|
||||
"""Class to represent Fan capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||
class LockCapabilities(AlexaEntity):
|
||||
"""Class to represent Lock capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.SMARTLOCK]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
return [AlexaLockController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
||||
class MediaPlayerCapabilities(AlexaEntity):
|
||||
"""Class to represent MediaPlayer capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.TV]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & media_player.const.SUPPORT_VOLUME_SET:
|
||||
yield AlexaSpeaker(self.entity)
|
||||
|
||||
step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE |
|
||||
media_player.const.SUPPORT_VOLUME_STEP)
|
||||
if supported & step_volume_features:
|
||||
yield AlexaStepSpeaker(self.entity)
|
||||
|
||||
playback_features = (media_player.const.SUPPORT_PLAY |
|
||||
media_player.const.SUPPORT_PAUSE |
|
||||
media_player.const.SUPPORT_STOP |
|
||||
media_player.const.SUPPORT_NEXT_TRACK |
|
||||
media_player.const.SUPPORT_PREVIOUS_TRACK)
|
||||
if supported & playback_features:
|
||||
yield AlexaPlaybackController(self.entity)
|
||||
|
||||
if supported & media_player.SUPPORT_SELECT_SOURCE:
|
||||
yield AlexaInputController(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(scene.DOMAIN)
|
||||
class SceneCapabilities(AlexaEntity):
|
||||
"""Class to represent Scene capabilities."""
|
||||
|
||||
def description(self):
|
||||
"""Return the description of the entity."""
|
||||
# Required description as per Amazon Scene docs
|
||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||
return scene_fmt.format(AlexaEntity.description(self))
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
return [AlexaSceneController(self.entity,
|
||||
supports_deactivation=False)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||
class ScriptCapabilities(AlexaEntity):
|
||||
"""Class to represent Script capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.ACTIVITY_TRIGGER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||
return [AlexaSceneController(self.entity,
|
||||
supports_deactivation=can_cancel)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||
class SensorCapabilities(AlexaEntity):
|
||||
"""Class to represent Sensor capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
# although there are other kinds of sensors, all but temperature
|
||||
# sensors are currently ignored.
|
||||
return [DisplayCategory.TEMPERATURE_SENSOR]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
|
||||
class BinarySensorCapabilities(AlexaEntity):
|
||||
"""Class to represent BinarySensor capabilities."""
|
||||
|
||||
TYPE_CONTACT = 'contact'
|
||||
TYPE_MOTION = 'motion'
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
sensor_type = self.get_type()
|
||||
if sensor_type is self.TYPE_CONTACT:
|
||||
return [DisplayCategory.CONTACT_SENSOR]
|
||||
if sensor_type is self.TYPE_MOTION:
|
||||
return [DisplayCategory.MOTION_SENSOR]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
sensor_type = self.get_type()
|
||||
if sensor_type is self.TYPE_CONTACT:
|
||||
yield AlexaContactSensor(self.hass, self.entity)
|
||||
elif sensor_type is self.TYPE_MOTION:
|
||||
yield AlexaMotionSensor(self.hass, self.entity)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
|
||||
def get_type(self):
|
||||
"""Return the type of binary sensor."""
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(ATTR_DEVICE_CLASS) in (
|
||||
'door',
|
||||
'garage_door',
|
||||
'opening',
|
||||
'window',
|
||||
):
|
||||
return self.TYPE_CONTACT
|
||||
if attrs.get(ATTR_DEVICE_CLASS) == 'motion':
|
||||
return self.TYPE_MOTION
|
91
homeassistant/components/alexa/errors.py
Normal file
91
homeassistant/components/alexa/errors.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Alexa related errors."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import API_TEMP_UNITS
|
||||
|
||||
|
||||
class UnsupportedInterface(HomeAssistantError):
|
||||
"""This entity does not support the requested Smart Home API interface."""
|
||||
|
||||
|
||||
class UnsupportedProperty(HomeAssistantError):
|
||||
"""This entity does not support the requested Smart Home API property."""
|
||||
|
||||
|
||||
class NoTokenAvailable(HomeAssistantError):
|
||||
"""There is no access token available."""
|
||||
|
||||
|
||||
class AlexaError(Exception):
|
||||
"""Base class for errors that can be serialized for the Alexa API.
|
||||
|
||||
A handler can raise subclasses of this to return an error to the request.
|
||||
"""
|
||||
|
||||
namespace = None
|
||||
error_type = None
|
||||
|
||||
def __init__(self, error_message, payload=None):
|
||||
"""Initialize an alexa error."""
|
||||
Exception.__init__(self)
|
||||
self.error_message = error_message
|
||||
self.payload = None
|
||||
|
||||
|
||||
class AlexaInvalidEndpointError(AlexaError):
|
||||
"""The endpoint in the request does not exist."""
|
||||
|
||||
namespace = 'Alexa'
|
||||
error_type = 'NO_SUCH_ENDPOINT'
|
||||
|
||||
def __init__(self, endpoint_id):
|
||||
"""Initialize invalid endpoint error."""
|
||||
msg = 'The endpoint {} does not exist'.format(endpoint_id)
|
||||
AlexaError.__init__(self, msg)
|
||||
self.endpoint_id = endpoint_id
|
||||
|
||||
|
||||
class AlexaInvalidValueError(AlexaError):
|
||||
"""Class to represent InvalidValue errors."""
|
||||
|
||||
namespace = 'Alexa'
|
||||
error_type = 'INVALID_VALUE'
|
||||
|
||||
|
||||
class AlexaUnsupportedThermostatModeError(AlexaError):
|
||||
"""Class to represent UnsupportedThermostatMode errors."""
|
||||
|
||||
namespace = 'Alexa.ThermostatController'
|
||||
error_type = 'UNSUPPORTED_THERMOSTAT_MODE'
|
||||
|
||||
|
||||
class AlexaTempRangeError(AlexaError):
|
||||
"""Class to represent TempRange errors."""
|
||||
|
||||
namespace = 'Alexa'
|
||||
error_type = 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||
|
||||
def __init__(self, hass, temp, min_temp, max_temp):
|
||||
"""Initialize TempRange error."""
|
||||
unit = hass.config.units.temperature_unit
|
||||
temp_range = {
|
||||
'minimumValue': {
|
||||
'value': min_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
'maximumValue': {
|
||||
'value': max_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
payload = {'validRange': temp_range}
|
||||
msg = 'The requested temperature {} is out of range'.format(temp)
|
||||
|
||||
AlexaError.__init__(self, msg, payload)
|
||||
|
||||
|
||||
class AlexaBridgeUnreachableError(AlexaError):
|
||||
"""Class to represent BridgeUnreachable errors."""
|
||||
|
||||
namespace = 'Alexa'
|
||||
error_type = 'BRIDGE_UNREACHABLE'
|
735
homeassistant/components/alexa/handlers.py
Normal file
735
homeassistant/components/alexa/handlers.py
Normal file
@@ -0,0 +1,735 @@
|
||||
"""Alexa message handlers."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant import core as ha
|
||||
from homeassistant.components import cover, fan, group, light, media_player
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import (
|
||||
API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause)
|
||||
from .entities import async_get_entities
|
||||
from .errors import (
|
||||
AlexaInvalidValueError, AlexaTempRangeError,
|
||||
AlexaUnsupportedThermostatModeError)
|
||||
from .state_report import async_enable_proactive_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
HANDLERS = Registry()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
async def async_api_discovery(hass, config, directive, context):
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
discovery_endpoints = [
|
||||
alexa_entity.serialize_discovery()
|
||||
for alexa_entity in async_get_entities(hass, config)
|
||||
if config.should_expose(alexa_entity.entity_id)
|
||||
]
|
||||
|
||||
return directive.response(
|
||||
name='Discover.Response',
|
||||
namespace='Alexa.Discovery',
|
||||
payload={'endpoints': discovery_endpoints},
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
|
||||
async def async_api_accept_grant(hass, config, directive, context):
|
||||
"""Create a API formatted AcceptGrant response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
auth_code = directive.payload['grant']['code']
|
||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
||||
|
||||
if config.supports_auth:
|
||||
await config.async_accept_grant(auth_code)
|
||||
|
||||
if config.should_report_state:
|
||||
await async_enable_proactive_mode(hass, config)
|
||||
|
||||
return directive.response(
|
||||
name='AcceptGrant.Response',
|
||||
namespace='Alexa.Authorization',
|
||||
payload={})
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
async def async_api_turn_on(hass, config, directive, context):
|
||||
"""Process a turn on request."""
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
if domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
service = SERVICE_TURN_ON
|
||||
if domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif domain == media_player.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
power_features = (media_player.SUPPORT_TURN_ON |
|
||||
media_player.SUPPORT_TURN_OFF)
|
||||
if not supported & power_features:
|
||||
service = media_player.SERVICE_MEDIA_PLAY
|
||||
|
||||
await hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
async def async_api_turn_off(hass, config, directive, context):
|
||||
"""Process a turn off request."""
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif domain == media_player.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
power_features = (media_player.SUPPORT_TURN_ON |
|
||||
media_player.SUPPORT_TURN_OFF)
|
||||
if not supported & power_features:
|
||||
service = media_player.SERVICE_MEDIA_STOP
|
||||
|
||||
await hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
async def async_api_set_brightness(hass, config, directive, context):
|
||||
"""Process a set brightness request."""
|
||||
entity = directive.entity
|
||||
brightness = int(directive.payload['brightness'])
|
||||
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
||||
async def async_api_adjust_brightness(hass, config, directive, context):
|
||||
"""Process an adjust brightness request."""
|
||||
entity = directive.entity
|
||||
brightness_delta = int(directive.payload['brightnessDelta'])
|
||||
|
||||
# read current state
|
||||
try:
|
||||
current = math.floor(
|
||||
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
|
||||
except ZeroDivisionError:
|
||||
current = 0
|
||||
|
||||
# set brightness
|
||||
brightness = max(0, brightness_delta + current)
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
||||
async def async_api_set_color(hass, config, directive, context):
|
||||
"""Process a set color request."""
|
||||
entity = directive.entity
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(directive.payload['color']['hue']),
|
||||
float(directive.payload['color']['saturation']),
|
||||
float(directive.payload['color']['brightness'])
|
||||
)
|
||||
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
||||
async def async_api_set_color_temperature(hass, config, directive, context):
|
||||
"""Process a set color temperature request."""
|
||||
entity = directive.entity
|
||||
kelvin = int(directive.payload['colorTemperatureInKelvin'])
|
||||
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
||||
async def async_api_decrease_color_temp(hass, config, directive, context):
|
||||
"""Process a decrease color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
|
||||
value = min(max_mireds, current + 50)
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
||||
async def async_api_increase_color_temp(hass, config, directive, context):
|
||||
"""Process an increase color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
|
||||
value = max(min_mireds, current - 50)
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
|
||||
async def async_api_activate(hass, config, directive, context):
|
||||
"""Process an activate request."""
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
|
||||
await hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': Cause.VOICE_INTERACTION},
|
||||
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
||||
}
|
||||
|
||||
return directive.response(
|
||||
name='ActivationStarted',
|
||||
namespace='Alexa.SceneController',
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
|
||||
async def async_api_deactivate(hass, config, directive, context):
|
||||
"""Process a deactivate request."""
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
|
||||
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': Cause.VOICE_INTERACTION},
|
||||
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
||||
}
|
||||
|
||||
return directive.response(
|
||||
name='DeactivationStarted',
|
||||
namespace='Alexa.SceneController',
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||
async def async_api_set_percentage(hass, config, directive, context):
|
||||
"""Process a set percentage request."""
|
||||
entity = directive.entity
|
||||
percentage = int(directive.payload['percentage'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
|
||||
async def async_api_adjust_percentage(hass, config, directive, context):
|
||||
"""Process an adjust percentage request."""
|
||||
entity = directive.entity
|
||||
percentage_delta = int(directive.payload['percentageDelta'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
if speed == "off":
|
||||
current = 0
|
||||
elif speed == "low":
|
||||
current = 33
|
||||
elif speed == "medium":
|
||||
current = 66
|
||||
elif speed == "high":
|
||||
current = 100
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.LockController', 'Lock'))
|
||||
async def async_api_lock(hass, config, directive, context):
|
||||
"""Process a lock request."""
|
||||
entity = directive.entity
|
||||
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
response = directive.response()
|
||||
response.add_context_property({
|
||||
'name': 'lockState',
|
||||
'namespace': 'Alexa.LockController',
|
||||
'value': 'LOCKED'
|
||||
})
|
||||
return response
|
||||
|
||||
|
||||
# Not supported by Alexa yet
|
||||
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
|
||||
async def async_api_unlock(hass, config, directive, context):
|
||||
"""Process an unlock request."""
|
||||
entity = directive.entity
|
||||
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
|
||||
async def async_api_set_volume(hass, config, directive, context):
|
||||
"""Process a set volume request."""
|
||||
volume = round(float(directive.payload['volume'] / 100), 2)
|
||||
entity = directive.entity
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
|
||||
async def async_api_select_input(hass, config, directive, context):
|
||||
"""Process a set input request."""
|
||||
media_input = directive.payload['input']
|
||||
entity = directive.entity
|
||||
|
||||
# attempt to map the ALL UPPERCASE payload name to a source
|
||||
source_list = entity.attributes[
|
||||
media_player.const.ATTR_INPUT_SOURCE_LIST] or []
|
||||
for source in source_list:
|
||||
# response will always be space separated, so format the source in the
|
||||
# most likely way to find a match
|
||||
formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
|
||||
if formatted_source in media_input.lower():
|
||||
media_input = source
|
||||
break
|
||||
else:
|
||||
msg = 'failed to map input {} to a media source on {}'.format(
|
||||
media_input, entity.entity_id)
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_SELECT_SOURCE,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
||||
async def async_api_adjust_volume(hass, config, directive, context):
|
||||
"""Process an adjust volume request."""
|
||||
volume_delta = int(directive.payload['volume'])
|
||||
|
||||
entity = directive.entity
|
||||
current_level = entity.attributes.get(
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
# read current state
|
||||
try:
|
||||
current = math.floor(int(current_level * 100))
|
||||
except ZeroDivisionError:
|
||||
current = 0
|
||||
|
||||
volume = float(max(0, volume_delta + current) / 100)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
|
||||
async def async_api_adjust_volume_step(hass, config, directive, context):
|
||||
"""Process an adjust volume step request."""
|
||||
# media_player volume up/down service does not support specifying steps
|
||||
# each component handles it differently e.g. via config.
|
||||
# For now we use the volumeSteps returned to figure out if we
|
||||
# should step up/down
|
||||
volume_step = directive.payload['volumeSteps']
|
||||
entity = directive.entity
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}
|
||||
|
||||
if volume_step > 0:
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_UP,
|
||||
data, blocking=False, context=context)
|
||||
elif volume_step < 0:
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_DOWN,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
||||
async def async_api_set_mute(hass, config, directive, context):
|
||||
"""Process a set mute request."""
|
||||
mute = bool(directive.payload['mute'])
|
||||
entity = directive.entity
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_MUTE,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
|
||||
async def async_api_play(hass, config, directive, context):
|
||||
"""Process a play request."""
|
||||
entity = directive.entity
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
|
||||
async def async_api_pause(hass, config, directive, context):
|
||||
"""Process a pause request."""
|
||||
entity = directive.entity
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
|
||||
async def async_api_stop(hass, config, directive, context):
|
||||
"""Process a stop request."""
|
||||
entity = directive.entity
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
|
||||
async def async_api_next(hass, config, directive, context):
|
||||
"""Process a next request."""
|
||||
entity = directive.entity
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
|
||||
async def async_api_previous(hass, config, directive, context):
|
||||
"""Process a previous request."""
|
||||
entity = directive.entity
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
def temperature_from_object(hass, temp_obj, interval=False):
|
||||
"""Get temperature from Temperature object in requested unit."""
|
||||
to_unit = hass.config.units.temperature_unit
|
||||
from_unit = TEMP_CELSIUS
|
||||
temp = float(temp_obj['value'])
|
||||
|
||||
if temp_obj['scale'] == 'FAHRENHEIT':
|
||||
from_unit = TEMP_FAHRENHEIT
|
||||
elif temp_obj['scale'] == 'KELVIN':
|
||||
# convert to Celsius if absolute temperature
|
||||
if not interval:
|
||||
temp -= 273.15
|
||||
|
||||
return convert_temperature(temp, from_unit, to_unit, interval)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||
async def async_api_set_target_temp(hass, config, directive, context):
|
||||
"""Process a set target temperature request."""
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
unit = hass.config.units.temperature_unit
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
payload = directive.payload
|
||||
response = directive.response()
|
||||
if 'targetSetpoint' in payload:
|
||||
temp = temperature_from_object(hass, payload['targetSetpoint'])
|
||||
if temp < min_temp or temp > max_temp:
|
||||
raise AlexaTempRangeError(hass, temp, min_temp, max_temp)
|
||||
data[ATTR_TEMPERATURE] = temp
|
||||
response.add_context_property({
|
||||
'name': 'targetSetpoint',
|
||||
'namespace': 'Alexa.ThermostatController',
|
||||
'value': {'value': temp, 'scale': API_TEMP_UNITS[unit]},
|
||||
})
|
||||
if 'lowerSetpoint' in payload:
|
||||
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
|
||||
if temp_low < min_temp or temp_low > max_temp:
|
||||
raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp)
|
||||
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||
response.add_context_property({
|
||||
'name': 'lowerSetpoint',
|
||||
'namespace': 'Alexa.ThermostatController',
|
||||
'value': {'value': temp_low, 'scale': API_TEMP_UNITS[unit]},
|
||||
})
|
||||
if 'upperSetpoint' in payload:
|
||||
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
|
||||
if temp_high < min_temp or temp_high > max_temp:
|
||||
raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp)
|
||||
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||
response.add_context_property({
|
||||
'name': 'upperSetpoint',
|
||||
'namespace': 'Alexa.ThermostatController',
|
||||
'value': {'value': temp_high, 'scale': API_TEMP_UNITS[unit]},
|
||||
})
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
|
||||
context=context)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||
async def async_api_adjust_target_temp(hass, config, directive, context):
|
||||
"""Process an adjust target temperature request."""
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
unit = hass.config.units.temperature_unit
|
||||
|
||||
temp_delta = temperature_from_object(
|
||||
hass, directive.payload['targetSetpointDelta'], interval=True)
|
||||
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||
|
||||
if target_temp < min_temp or target_temp > max_temp:
|
||||
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_TEMPERATURE: target_temp,
|
||||
}
|
||||
|
||||
response = directive.response()
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
|
||||
context=context)
|
||||
response.add_context_property({
|
||||
'name': 'targetSetpoint',
|
||||
'namespace': 'Alexa.ThermostatController',
|
||||
'value': {'value': target_temp, 'scale': API_TEMP_UNITS[unit]},
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||
async def async_api_set_thermostat_mode(hass, config, directive, context):
|
||||
"""Process a set thermostat mode request."""
|
||||
entity = directive.entity
|
||||
mode = directive.payload['thermostatMode']
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}
|
||||
|
||||
ha_preset = next(
|
||||
(k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode),
|
||||
None
|
||||
)
|
||||
|
||||
if ha_preset:
|
||||
presets = entity.attributes.get(climate.ATTR_PRESET_MODES, [])
|
||||
|
||||
if ha_preset not in presets:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(
|
||||
ha_preset
|
||||
)
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
|
||||
service = climate.SERVICE_SET_PRESET_MODE
|
||||
data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO
|
||||
|
||||
else:
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(
|
||||
mode
|
||||
)
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
|
||||
service = climate.SERVICE_SET_HVAC_MODE
|
||||
data[climate.ATTR_HVAC_MODE] = ha_mode
|
||||
|
||||
response = directive.response()
|
||||
await hass.services.async_call(
|
||||
climate.DOMAIN, service, data,
|
||||
blocking=False, context=context)
|
||||
response.add_context_property({
|
||||
'name': 'thermostatMode',
|
||||
'namespace': 'Alexa.ThermostatController',
|
||||
'value': mode,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
async def async_api_reportstate(hass, config, directive, context):
|
||||
"""Process a ReportState request."""
|
||||
return directive.response(name='StateReport')
|
200
homeassistant/components/alexa/messages.py
Normal file
200
homeassistant/components/alexa/messages.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Alexa models."""
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from .const import (
|
||||
API_CONTEXT,
|
||||
API_DIRECTIVE,
|
||||
API_ENDPOINT,
|
||||
API_EVENT,
|
||||
API_HEADER,
|
||||
API_PAYLOAD,
|
||||
API_SCOPE,
|
||||
)
|
||||
from .entities import ENTITY_ADAPTERS
|
||||
from .errors import AlexaInvalidEndpointError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlexaDirective:
|
||||
"""An incoming Alexa directive."""
|
||||
|
||||
def __init__(self, request):
|
||||
"""Initialize a directive."""
|
||||
self._directive = request[API_DIRECTIVE]
|
||||
self.namespace = self._directive[API_HEADER]['namespace']
|
||||
self.name = self._directive[API_HEADER]['name']
|
||||
self.payload = self._directive[API_PAYLOAD]
|
||||
self.has_endpoint = API_ENDPOINT in self._directive
|
||||
|
||||
self.entity = self.entity_id = self.endpoint = None
|
||||
|
||||
def load_entity(self, hass, config):
|
||||
"""Set attributes related to the entity for this request.
|
||||
|
||||
Sets these attributes when self.has_endpoint is True:
|
||||
|
||||
- entity
|
||||
- entity_id
|
||||
- endpoint
|
||||
|
||||
Behavior when self.has_endpoint is False is undefined.
|
||||
|
||||
Will raise AlexaInvalidEndpointError if the endpoint in the request is
|
||||
malformed or nonexistant.
|
||||
"""
|
||||
_endpoint_id = self._directive[API_ENDPOINT]['endpointId']
|
||||
self.entity_id = _endpoint_id.replace('#', '.')
|
||||
|
||||
self.entity = hass.states.get(self.entity_id)
|
||||
if not self.entity or not config.should_expose(self.entity_id):
|
||||
raise AlexaInvalidEndpointError(_endpoint_id)
|
||||
|
||||
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](
|
||||
hass, config, self.entity)
|
||||
|
||||
def response(self,
|
||||
name='Response',
|
||||
namespace='Alexa',
|
||||
payload=None):
|
||||
"""Create an API formatted response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
response = AlexaResponse(name, namespace, payload)
|
||||
|
||||
token = self._directive[API_HEADER].get('correlationToken')
|
||||
if token:
|
||||
response.set_correlation_token(token)
|
||||
|
||||
if self.has_endpoint:
|
||||
response.set_endpoint(self._directive[API_ENDPOINT].copy())
|
||||
|
||||
return response
|
||||
|
||||
def error(
|
||||
self,
|
||||
namespace='Alexa',
|
||||
error_type='INTERNAL_ERROR',
|
||||
error_message="",
|
||||
payload=None
|
||||
):
|
||||
"""Create a API formatted error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = payload or {}
|
||||
payload['type'] = error_type
|
||||
payload['message'] = error_message
|
||||
|
||||
_LOGGER.info("Request %s/%s error %s: %s",
|
||||
self._directive[API_HEADER]['namespace'],
|
||||
self._directive[API_HEADER]['name'],
|
||||
error_type, error_message)
|
||||
|
||||
return self.response(
|
||||
name='ErrorResponse',
|
||||
namespace=namespace,
|
||||
payload=payload
|
||||
)
|
||||
|
||||
|
||||
class AlexaResponse:
|
||||
"""Class to hold a response."""
|
||||
|
||||
def __init__(self, name, namespace, payload=None):
|
||||
"""Initialize the response."""
|
||||
payload = payload or {}
|
||||
self._response = {
|
||||
API_EVENT: {
|
||||
API_HEADER: {
|
||||
'namespace': namespace,
|
||||
'name': name,
|
||||
'messageId': str(uuid4()),
|
||||
'payloadVersion': '3',
|
||||
},
|
||||
API_PAYLOAD: payload,
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this response."""
|
||||
return self._response[API_EVENT][API_HEADER]['name']
|
||||
|
||||
@property
|
||||
def namespace(self):
|
||||
"""Return the namespace of this response."""
|
||||
return self._response[API_EVENT][API_HEADER]['namespace']
|
||||
|
||||
def set_correlation_token(self, token):
|
||||
"""Set the correlationToken.
|
||||
|
||||
This should normally mirror the value from a request, and is set by
|
||||
AlexaDirective.response() usually.
|
||||
"""
|
||||
self._response[API_EVENT][API_HEADER]['correlationToken'] = token
|
||||
|
||||
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
|
||||
"""Set the endpoint dictionary.
|
||||
|
||||
This is used to send proactive messages to Alexa.
|
||||
"""
|
||||
self._response[API_EVENT][API_ENDPOINT] = {
|
||||
API_SCOPE: {
|
||||
'type': 'BearerToken',
|
||||
'token': bearer_token
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint_id is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id
|
||||
|
||||
if cookie is not None:
|
||||
self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie
|
||||
|
||||
def set_endpoint(self, endpoint):
|
||||
"""Set the endpoint.
|
||||
|
||||
This should normally mirror the value from a request, and is set by
|
||||
AlexaDirective.response() usually.
|
||||
"""
|
||||
self._response[API_EVENT][API_ENDPOINT] = endpoint
|
||||
|
||||
def _properties(self):
|
||||
context = self._response.setdefault(API_CONTEXT, {})
|
||||
return context.setdefault('properties', [])
|
||||
|
||||
def add_context_property(self, prop):
|
||||
"""Add a property to the response context.
|
||||
|
||||
The Alexa response includes a list of properties which provides
|
||||
feedback on how states have changed. For example if a user asks,
|
||||
"Alexa, set theromstat to 20 degrees", the API expects a response with
|
||||
the new value of the property, and Alexa will respond to the user
|
||||
"Thermostat set to 20 degrees".
|
||||
|
||||
async_handle_message() will call .merge_context_properties() for every
|
||||
request automatically, however often handlers will call services to
|
||||
change state but the effects of those changes are applied
|
||||
asynchronously. Thus, handlers should call this method to confirm
|
||||
changes before returning.
|
||||
"""
|
||||
self._properties().append(prop)
|
||||
|
||||
def merge_context_properties(self, endpoint):
|
||||
"""Add all properties from given endpoint if not already set.
|
||||
|
||||
Handlers should be using .add_context_property().
|
||||
"""
|
||||
properties = self._properties()
|
||||
already_set = {(p['namespace'], p['name']) for p in properties}
|
||||
|
||||
for prop in endpoint.serialize_properties():
|
||||
if (prop['namespace'], prop['name']) not in already_set:
|
||||
self.add_context_property(prop)
|
||||
|
||||
def serialize(self):
|
||||
"""Return response as a JSON-able data structure."""
|
||||
return self._response
|
File diff suppressed because it is too large
Load Diff
114
homeassistant/components/alexa/smart_home_http.py
Normal file
114
homeassistant/components/alexa/smart_home_http.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Alexa HTTP interface."""
|
||||
import logging
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
|
||||
from .auth import Auth
|
||||
from .config import AbstractConfig
|
||||
from .const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_ENDPOINT,
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER
|
||||
)
|
||||
from .state_report import async_enable_proactive_mode
|
||||
from .smart_home import async_handle_message
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
|
||||
class AlexaConfig(AbstractConfig):
|
||||
"""Alexa config."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize Alexa config."""
|
||||
super().__init__(hass)
|
||||
self._config = config
|
||||
|
||||
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
|
||||
self._auth = Auth(hass, config[CONF_CLIENT_ID],
|
||||
config[CONF_CLIENT_SECRET])
|
||||
else:
|
||||
self._auth = None
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
"""Return if config supports auth."""
|
||||
return self._auth is not None
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
"""Return if we should proactively report states."""
|
||||
return self._auth is not None
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
"""Endpoint for report state."""
|
||||
return self._config.get(CONF_ENDPOINT)
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
"""Return entity config."""
|
||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||
|
||||
def should_expose(self, entity_id):
|
||||
"""If an entity should be exposed."""
|
||||
return self._config[CONF_FILTER](entity_id)
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Get an access token."""
|
||||
return await self._auth.async_get_access_token()
|
||||
|
||||
async def async_accept_grant(self, code):
|
||||
"""Accept a grant."""
|
||||
return await self._auth.async_do_auth(code)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Activate Smart Home functionality of Alexa component.
|
||||
|
||||
This is optional, triggered by having a `smart_home:` sub-section in the
|
||||
alexa configuration.
|
||||
|
||||
Even if that's disabled, the functionality in this module may still be used
|
||||
by the cloud component which will call async_handle_message directly.
|
||||
"""
|
||||
smart_home_config = AlexaConfig(hass, config)
|
||||
hass.http.register_view(SmartHomeView(smart_home_config))
|
||||
|
||||
if smart_home_config.should_report_state:
|
||||
await async_enable_proactive_mode(hass, smart_home_config)
|
||||
|
||||
|
||||
class SmartHomeView(HomeAssistantView):
|
||||
"""Expose Smart Home v3 payload interface via HTTP POST."""
|
||||
|
||||
url = SMART_HOME_HTTP_ENDPOINT
|
||||
name = 'api:alexa:smart_home'
|
||||
|
||||
def __init__(self, smart_home_config):
|
||||
"""Initialize."""
|
||||
self.smart_home_config = smart_home_config
|
||||
|
||||
async def post(self, request):
|
||||
"""Handle Alexa Smart Home requests.
|
||||
|
||||
The Smart Home API requires the endpoint to be implemented in AWS
|
||||
Lambda, which will need to forward the requests to here and pass back
|
||||
the response.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
user = request['hass_user']
|
||||
message = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
|
||||
|
||||
response = await async_handle_message(
|
||||
hass, self.smart_home_config, message,
|
||||
context=core.Context(user_id=user.id)
|
||||
)
|
||||
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
||||
return b'' if response is None else self.json(response)
|
185
homeassistant/components/alexa/state_report.py
Normal file
185
homeassistant/components/alexa/state_report.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Alexa state report code."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import MATCH_ALL
|
||||
|
||||
from .const import API_CHANGE, Cause
|
||||
from .entities import ENTITY_ADAPTERS
|
||||
from .messages import AlexaResponse
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
|
||||
async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
"""Enable the proactive mode.
|
||||
|
||||
Proactive mode makes this component report state changes to Alexa.
|
||||
"""
|
||||
# Validate we can get access token.
|
||||
await smart_home_config.async_get_access_token()
|
||||
|
||||
async def async_entity_state_listener(changed_entity, old_state,
|
||||
new_state):
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if new_state.domain not in ENTITY_ADAPTERS:
|
||||
return
|
||||
|
||||
if not smart_home_config.should_expose(changed_entity):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
changed_entity)
|
||||
return
|
||||
|
||||
alexa_changed_entity = \
|
||||
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
|
||||
new_state)
|
||||
|
||||
for interface in alexa_changed_entity.interfaces():
|
||||
if interface.properties_proactively_reported():
|
||||
await async_send_changereport_message(hass, smart_home_config,
|
||||
alexa_changed_entity)
|
||||
return
|
||||
|
||||
return hass.helpers.event.async_track_state_change(
|
||||
MATCH_ALL, async_entity_state_listener
|
||||
)
|
||||
|
||||
|
||||
async def async_send_changereport_message(hass, config, alexa_entity):
|
||||
"""Send a ChangeReport message for an Alexa entity.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
|
||||
endpoint = alexa_entity.alexa_id()
|
||||
|
||||
# this sends all the properties of the Alexa Entity, whether they have
|
||||
# changed or not. this should be improved, and properties that have not
|
||||
# changed should be moved to the 'context' object
|
||||
properties = list(alexa_entity.serialize_properties())
|
||||
|
||||
payload = {
|
||||
API_CHANGE: {
|
||||
'cause': {'type': Cause.APP_INTERACTION},
|
||||
'properties': properties
|
||||
}
|
||||
}
|
||||
|
||||
message = AlexaResponse(name='ChangeReport', namespace='Alexa',
|
||||
payload=payload)
|
||||
message.set_endpoint_full(token, endpoint)
|
||||
|
||||
message_serialized = message.serialize()
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(config.endpoint,
|
||||
headers=headers,
|
||||
json=message_serialized,
|
||||
allow_redirects=True)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout sending report to Alexa.")
|
||||
return None
|
||||
|
||||
response_text = await response.text()
|
||||
|
||||
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
|
||||
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
||||
|
||||
if response.status != 202:
|
||||
response_json = json.loads(response_text)
|
||||
_LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s",
|
||||
response_json["payload"]["code"],
|
||||
response_json["payload"]["description"])
|
||||
|
||||
|
||||
async def async_send_add_or_update_message(hass, config, entity_ids):
|
||||
"""Send an AddOrUpdateReport message for entities.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#add-or-update-report
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
|
||||
endpoints = []
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = entity_id.split('.', 1)[0]
|
||||
alexa_entity = ENTITY_ADAPTERS[domain](
|
||||
hass, config, hass.states.get(entity_id)
|
||||
)
|
||||
endpoints.append(alexa_entity.serialize_discovery())
|
||||
|
||||
payload = {
|
||||
'endpoints': endpoints,
|
||||
'scope': {
|
||||
'type': 'BearerToken',
|
||||
'token': token,
|
||||
}
|
||||
}
|
||||
|
||||
message = AlexaResponse(
|
||||
name='AddOrUpdateReport', namespace='Alexa.Discovery', payload=payload)
|
||||
|
||||
message_serialized = message.serialize()
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
return await session.post(config.endpoint, headers=headers,
|
||||
json=message_serialized, allow_redirects=True)
|
||||
|
||||
|
||||
async def async_send_delete_message(hass, config, entity_ids):
|
||||
"""Send an DeleteReport message for entities.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event
|
||||
"""
|
||||
token = await config.async_get_access_token()
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
|
||||
endpoints = []
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = entity_id.split('.', 1)[0]
|
||||
alexa_entity = ENTITY_ADAPTERS[domain](
|
||||
hass, config, hass.states.get(entity_id)
|
||||
)
|
||||
endpoints.append({
|
||||
'endpointId': alexa_entity.alexa_id()
|
||||
})
|
||||
|
||||
payload = {
|
||||
'endpoints': endpoints,
|
||||
'scope': {
|
||||
'type': 'BearerToken',
|
||||
'token': token,
|
||||
}
|
||||
}
|
||||
|
||||
message = AlexaResponse(name='DeleteReport', namespace='Alexa.Discovery',
|
||||
payload=payload)
|
||||
|
||||
message_serialized = message.serialize()
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
return await session.post(config.endpoint, headers=headers,
|
||||
json=message_serialized, allow_redirects=True)
|
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Error desconocido al generar un token de acceso.",
|
||||
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
|
||||
"no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. Por favor, lea las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticaci\u00f3n exitosa con Ambiclimate"
|
||||
}
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/es.json
Normal file
23
homeassistant/components/ambiclimate/.translations/es.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Error desconocido al generar un token de acceso.",
|
||||
"already_setup": "La cuenta de Ambiclimate est\u00e1 configurada.",
|
||||
"no_config": "Es necesario configurar Ambiclimate antes de poder autenticarse con \u00e9l. [Por favor, lee las instrucciones](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticado correctamente con Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.",
|
||||
"no_token": "No autenticado con Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Accede al siguiente [enlace]({authorization_url}) y <b>permite</b> el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en <b>enviar</b> a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})",
|
||||
"title": "Autenticaci\u00f3n de Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal file
23
homeassistant/components/ambiclimate/.translations/fr.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
|
||||
"already_setup": "Le compte Ambiclimate est configur\u00e9.",
|
||||
"no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
|
||||
"no_token": "Non authentifi\u00e9 avec Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Suivez ce [lien] ( {authorization_url} ) et <b> Autorisez </b> l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur <b> Envoyer </b> ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )",
|
||||
"title": "Authentifier Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_setup": "L'account Ambiclimate \u00e8 configurato."
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/nl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/nl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Onbekende fout bij het genereren van een toegangstoken.",
|
||||
"already_setup": "Het Ambiclimate-account is geconfigureerd.",
|
||||
"no_config": "U moet Ambiclimate configureren voordat u zich ermee kunt authenticeren. (Lees de instructies) (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Succesvol geverifieerd met Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Gelieve de link te volgen en te verifi\u00ebren voordat u op Verzenden drukt.",
|
||||
"no_token": "Niet geverifieerd met Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Volg deze [link] ( {authorization_url} ) en <b> Toestaan </b> toegang tot uw Ambiclimate-account, kom dan terug en druk hieronder op <b> Verzenden </b> . \n (Zorg ervoor dat de opgegeven callback-URL {cb_url} )",
|
||||
"title": "Authenticatie Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/no.json
Normal file
23
homeassistant/components/ambiclimate/.translations/no.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Ukjent feil ved oppretting av tilgangstoken.",
|
||||
"already_setup": "Ambiclimate-kontoen er konfigurert.",
|
||||
"no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Vellykket autentisering med Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send",
|
||||
"no_token": "Ikke autentisert med Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og <b>Tillat</b> tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk <b>Send</b> nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
|
||||
"title": "Autensiere Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/pl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.",
|
||||
"already_setup": "Konto Ambiclimate jest skonfigurowane.",
|
||||
"no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Pomy\u015blnie uwierzytelniono z Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij",
|
||||
"no_token": "Nie uwierzytelniony z Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Kliknij poni\u017cszy [link]({authorization_url}) i <b>Zezw\u00f3l</b> na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij <b>Prze\u015blij</b> poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
|
||||
"title": "Uwierzytelnienie Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Erro desconhecido ao gerar um token de acesso.",
|
||||
"already_setup": "A conta Ambiclimate est\u00e1 configurada.",
|
||||
"no_config": "Voc\u00ea precisa configurar o Ambiclimate antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticado com sucesso no Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Por favor, siga o link e autentique-se antes de pressionar Enviar",
|
||||
"no_token": "N\u00e3o autenticado com o Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Por favor, siga este [link]({authorization_url}) e <b>Permitir</b> acesso \u00e0 sua conta Ambiclimate, em seguida, volte e pressione <b>Enviar</b> abaixo. \n (Verifique se a URL de retorno de chamada especificada \u00e9 {cb_url})",
|
||||
"title": "Autenticar Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.",
|
||||
"already_setup": "Ra\u010dun Ambiclimate je konfiguriran.",
|
||||
"no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Uspe\u0161no overjeno z funkcijo Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost",
|
||||
"no_token": "Ni overjeno z Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Sledite temu povezavi ( {authorization_url} in <b> Dovoli </b> dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite <b> Po\u0161lji </b> spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )",
|
||||
"title": "Overi Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal file
23
homeassistant/components/ambiclimate/.translations/sv.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.",
|
||||
"already_setup": "Ambiclientkontot \u00e4r konfigurerat",
|
||||
"no_config": "Du m\u00e5ste konfigurera Ambiclimate innan du kan autentisera med den. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Lyckad autentisering med Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera dig innan du trycker p\u00e5 Skicka",
|
||||
"no_token": "Inte autentiserad med Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "V\u00e4nligen f\u00f6lj denna [l\u00e4nk] ({authorization_url}) och <b> till\u00e5ta </b> till g\u00e5ng till ditt Ambiclimate konto, kom sedan tillbaka och tryck p\u00e5 <b> Skicka </b> nedan.\n(Kontrollera att den angivna callback url \u00e4r {cb_url})",
|
||||
"title": "Autentisera Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
@@ -7,11 +7,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_ON_OFF, STATE_HEAT)
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.const import (ATTR_TEMPERATURE,
|
||||
STATE_OFF, TEMP_CELSIUS)
|
||||
SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, HVAC_MODE_HEAT)
|
||||
from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
|
||||
@@ -20,8 +17,7 @@ from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_ON_OFF)
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
@@ -56,14 +52,15 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
websession)
|
||||
|
||||
try:
|
||||
_token_info = await oauth.refresh_access_token(token_info)
|
||||
token_info = await oauth.refresh_access_token(token_info)
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
token_info = None
|
||||
|
||||
if not token_info:
|
||||
_LOGGER.error("Failed to refresh access token")
|
||||
return
|
||||
|
||||
if _token_info:
|
||||
await store.async_save(token_info)
|
||||
token_info = _token_info
|
||||
await store.async_save(token_info)
|
||||
|
||||
data_connection = ambiclimate.AmbiclimateConnection(oauth,
|
||||
token_info=token_info,
|
||||
@@ -176,11 +173,6 @@ class AmbiclimateEntity(ClimateDevice):
|
||||
"""Return the current humidity."""
|
||||
return self._data.get('humidity')
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if heater is on."""
|
||||
return self._data.get('power', '').lower() == 'on'
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
@@ -197,9 +189,17 @@ class AmbiclimateEntity(ClimateDevice):
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available hvac operation modes."""
|
||||
return [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT if self.is_on else STATE_OFF
|
||||
if self._data.get('power', '').lower() == 'on':
|
||||
return HVAC_MODE_HEAT
|
||||
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -208,13 +208,13 @@ class AmbiclimateEntity(ClimateDevice):
|
||||
return
|
||||
await self._heater.set_target_temperature(temperature)
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn device on."""
|
||||
await self._heater.turn_on()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn device off."""
|
||||
await self._heater.turn_off()
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
await self._heater.turn_on()
|
||||
return
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
await self._heater.turn_off()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
|
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "ambiclimate",
|
||||
"name": "Ambiclimate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambiclimate",
|
||||
"requirements": [
|
||||
"ambiclimate==0.1.1"
|
||||
"ambiclimate==0.2.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@@ -119,8 +119,8 @@ TYPE_WINDSPEEDMPH = 'windspeedmph'
|
||||
TYPE_YEARLYRAININ = 'yearlyrainin'
|
||||
SENSOR_TYPES = {
|
||||
TYPE_24HOURRAININ: ('24 Hr Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, None),
|
||||
TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, None),
|
||||
TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
|
||||
TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
|
||||
TYPE_BATT10: ('Battery 10', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_BATT1: ('Battery 1', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_BATT2: ('Battery 2', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
@@ -134,23 +134,23 @@ SENSOR_TYPES = {
|
||||
TYPE_BATTOUT: ('Battery', None, TYPE_BINARY_SENSOR, 'battery'),
|
||||
TYPE_CO2: ('co2', 'ppm', TYPE_SENSOR, None),
|
||||
TYPE_DAILYRAININ: ('Daily Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, None),
|
||||
TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_EVENTRAININ: ('Event Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, None),
|
||||
TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_HOURLYRAININ: ('Hourly Rain Rate', 'in/hr', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, None),
|
||||
TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, None),
|
||||
TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, 'timestamp'),
|
||||
TYPE_MAXDAILYGUST: ('Max Gust', 'mph', TYPE_SENSOR, None),
|
||||
TYPE_MONTHLYRAININ: ('Monthly Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_RELAY10: ('Relay 10', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
@@ -163,39 +163,39 @@ SENSOR_TYPES = {
|
||||
TYPE_RELAY7: ('Relay 7', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_RELAY8: ('Relay 8', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_RELAY9: ('Relay 9', None, TYPE_BINARY_SENSOR, 'connectivity'),
|
||||
TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOLARRADIATION: ('Solar Rad', 'W/m^2', TYPE_SENSOR, None),
|
||||
TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, None),
|
||||
TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, None),
|
||||
TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, 'humidity'),
|
||||
TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_SOLARRADIATION: ('Solar Rad', 'lx', TYPE_SENSOR, 'illuminance'),
|
||||
TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, 'temperature'),
|
||||
TYPE_TOTALRAININ: ('Lifetime Rain', 'in', TYPE_SENSOR, None),
|
||||
TYPE_UV: ('uv', 'Index', TYPE_SENSOR, None),
|
||||
TYPE_WEEKLYRAININ: ('Weekly Rain', 'in', TYPE_SENSOR, None),
|
||||
@@ -327,7 +327,7 @@ class AmbientStation:
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info('Connected to websocket')
|
||||
_LOGGER.debug('Watchdog starting')
|
||||
if self._watchdog_listener:
|
||||
if self._watchdog_listener is not None:
|
||||
self._watchdog_listener()
|
||||
self._watchdog_listener = async_call_later(
|
||||
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
|
||||
@@ -404,9 +404,10 @@ class AmbientWeatherEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type,
|
||||
sensor_name):
|
||||
sensor_name, device_class):
|
||||
"""Initialize the sensor."""
|
||||
self._ambient = ambient
|
||||
self._device_class = device_class
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._mac_address = mac_address
|
||||
self._sensor_name = sensor_name
|
||||
@@ -420,6 +421,11 @@ class AmbientWeatherEntity(Entity):
|
||||
return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._sensor_type) is not None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device registry information for this entity."""
|
||||
|
@@ -39,20 +39,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice):
|
||||
"""Define an Ambient binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type, sensor_name,
|
||||
device_class):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
ambient, mac_address, station_name, sensor_type, sensor_name)
|
||||
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
|
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "ambient_station",
|
||||
"name": "Ambient station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambient_station",
|
||||
"requirements": [
|
||||
"aioambient==0.3.0"
|
||||
"aioambient==0.3.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
|
||||
from . import SENSOR_TYPES, AmbientWeatherEntity
|
||||
from . import SENSOR_TYPES, TYPE_SOLARRADIATION, AmbientWeatherEntity
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -22,12 +22,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in ambient.monitored_conditions:
|
||||
name, unit, kind, _ = SENSOR_TYPES[condition]
|
||||
name, unit, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == TYPE_SENSOR:
|
||||
sensor_list.append(
|
||||
AmbientWeatherSensor(
|
||||
ambient, mac_address, station[ATTR_NAME], condition,
|
||||
name, unit))
|
||||
name, device_class, unit))
|
||||
|
||||
async_add_entities(sensor_list, True)
|
||||
|
||||
@@ -37,10 +37,15 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
|
||||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type, sensor_name,
|
||||
unit):
|
||||
device_class, unit):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
ambient, mac_address, station_name, sensor_type, sensor_name)
|
||||
ambient,
|
||||
mac_address,
|
||||
station_name,
|
||||
sensor_type,
|
||||
sensor_name,
|
||||
device_class)
|
||||
|
||||
self._unit = unit
|
||||
|
||||
@@ -56,5 +61,13 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._state = self._ambient.stations[
|
||||
new_state = self._ambient.stations[
|
||||
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
|
||||
|
||||
if self._sensor_type == TYPE_SOLARRADIATION:
|
||||
# Ambient's units for solar radiation (illuminance) are
|
||||
# W/m^2; since those aren't commonly used in the HASS
|
||||
# world, transform them to lx:
|
||||
self._state = round(float(new_state)/0.0079)
|
||||
else:
|
||||
self._state = new_state
|
||||
|
@@ -1,8 +1,10 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import threading
|
||||
|
||||
import aiohttp
|
||||
from amcrest import AmcrestError, Http, LoginError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
@@ -17,12 +19,14 @@ from homeassistant.const import (
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, dispatcher_send)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .binary_sensor import BINARY_SENSORS
|
||||
from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .const import DOMAIN, DATA_AMCREST
|
||||
from .const import CAMERAS, DOMAIN, DATA_AMCREST, DEVICES, SERVICE_UPDATE
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_MOTION_DETECTOR, SENSORS
|
||||
from .switch import SWITCHES
|
||||
@@ -32,11 +36,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
CONF_CONTROL_LIGHT = 'control_light'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
MAX_ERRORS = 5
|
||||
RECHECK_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
@@ -56,20 +63,21 @@ AUTHENTICATION_LIST = {
|
||||
def _deprecated_sensor_values(sensors):
|
||||
if SENSOR_MOTION_DETECTOR in sensors:
|
||||
_LOGGER.warning(
|
||||
"The 'sensors' option value '%s' is deprecated, "
|
||||
"The '%s' option value '%s' is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"the 'binary_sensors' option with value 'motion_detected' "
|
||||
"instead.", SENSOR_MOTION_DETECTOR)
|
||||
"the '%s' option with value '%s' instead",
|
||||
CONF_SENSORS, SENSOR_MOTION_DETECTOR, CONF_BINARY_SENSORS,
|
||||
BINARY_SENSOR_MOTION_DETECTED)
|
||||
return sensors
|
||||
|
||||
|
||||
def _deprecated_switches(config):
|
||||
if CONF_SWITCHES in config:
|
||||
_LOGGER.warning(
|
||||
"The 'switches' option (with value %s) is deprecated, "
|
||||
"The '%s' option (with value %s) is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"camera services and attributes instead.",
|
||||
config[CONF_SWITCHES])
|
||||
"services and attributes instead",
|
||||
CONF_SWITCHES, config[CONF_SWITCHES])
|
||||
return config
|
||||
|
||||
|
||||
@@ -103,6 +111,7 @@ AMCREST_SCHEMA = vol.All(
|
||||
_deprecated_sensor_values),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean,
|
||||
}),
|
||||
_deprecated_switches
|
||||
)
|
||||
@@ -112,35 +121,81 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class AmcrestChecker(Http):
|
||||
"""amcrest.Http wrapper for catching errors."""
|
||||
|
||||
def __init__(self, hass, name, host, port, user, password):
|
||||
"""Initialize."""
|
||||
self._hass = hass
|
||||
self._wrap_name = name
|
||||
self._wrap_errors = 0
|
||||
self._wrap_lock = threading.Lock()
|
||||
self._unsub_recheck = None
|
||||
super().__init__(host, port, user, password, retries_connection=1,
|
||||
timeout_protocol=3.05)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if camera's API is responding."""
|
||||
return self._wrap_errors <= MAX_ERRORS
|
||||
|
||||
def command(self, cmd, retries=None, timeout_cmd=None, stream=False):
|
||||
"""amcrest.Http.command wrapper to catch errors."""
|
||||
try:
|
||||
ret = super().command(cmd, retries, timeout_cmd, stream)
|
||||
except AmcrestError:
|
||||
with self._wrap_lock:
|
||||
was_online = self.available
|
||||
self._wrap_errors += 1
|
||||
_LOGGER.debug('%s camera errs: %i', self._wrap_name,
|
||||
self._wrap_errors)
|
||||
offline = not self.available
|
||||
if offline and was_online:
|
||||
_LOGGER.error(
|
||||
'%s camera offline: Too many errors', self._wrap_name)
|
||||
dispatcher_send(
|
||||
self._hass,
|
||||
service_signal(SERVICE_UPDATE, self._wrap_name))
|
||||
self._unsub_recheck = track_time_interval(
|
||||
self._hass, self._wrap_test_online, RECHECK_INTERVAL)
|
||||
raise
|
||||
with self._wrap_lock:
|
||||
was_offline = not self.available
|
||||
self._wrap_errors = 0
|
||||
if was_offline:
|
||||
self._unsub_recheck()
|
||||
self._unsub_recheck = None
|
||||
_LOGGER.error('%s camera back online', self._wrap_name)
|
||||
dispatcher_send(
|
||||
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name))
|
||||
return ret
|
||||
|
||||
def _wrap_test_online(self, now):
|
||||
"""Test if camera is back online."""
|
||||
try:
|
||||
self.current_time
|
||||
except AmcrestError:
|
||||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera, AmcrestError
|
||||
hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []})
|
||||
|
||||
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
|
||||
devices = config[DOMAIN]
|
||||
|
||||
for device in devices:
|
||||
for device in config[DOMAIN]:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
api = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
# Test camera communications.
|
||||
api.current_time
|
||||
api = AmcrestChecker(
|
||||
hass, name,
|
||||
device[CONF_HOST], device[CONF_PORT],
|
||||
username, password)
|
||||
|
||||
except AmcrestError as ex:
|
||||
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
except LoginError as ex:
|
||||
_LOGGER.error("Login error for %s camera: %s", name, ex)
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
|
||||
@@ -149,6 +204,7 @@ def setup(hass, config):
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = device[CONF_STREAM_SOURCE]
|
||||
control_light = device.get(CONF_CONTROL_LIGHT)
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
@@ -157,9 +213,9 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
|
||||
hass.data[DATA_AMCREST][DEVICES][name] = AmcrestDevice(
|
||||
api, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
resolution, control_light)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, CAMERA, DOMAIN, {
|
||||
@@ -187,7 +243,7 @@ def setup(hass, config):
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
if not hass.data[DATA_AMCREST]['devices']:
|
||||
if not hass.data[DATA_AMCREST][DEVICES]:
|
||||
return False
|
||||
|
||||
def have_permission(user, entity_id):
|
||||
@@ -205,13 +261,13 @@ def setup(hass, config):
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id for entity_id in hass.data[DATA_AMCREST]['cameras']
|
||||
entity_id for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST]['cameras']:
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
@@ -245,10 +301,11 @@ class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, api, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
stream_source, resolution, control_light):
|
||||
"""Initialize the entity."""
|
||||
self.api = api
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
self.resolution = resolution
|
||||
self.control_light = control_light
|
||||
|
@@ -2,18 +2,27 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_MOTION)
|
||||
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
|
||||
from amcrest import AmcrestError
|
||||
|
||||
from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION)
|
||||
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (
|
||||
BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST, DEVICES, SERVICE_UPDATE)
|
||||
from .helpers import log_update_error, service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
BINARY_SENSOR_MOTION_DETECTED = 'motion_detected'
|
||||
BINARY_SENSOR_ONLINE = 'online'
|
||||
# Binary sensor types are defined like: Name, device class
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
BINARY_SENSOR_MOTION_DETECTED: ('Motion Detected', DEVICE_CLASS_MOTION),
|
||||
BINARY_SENSOR_ONLINE: ('Online', DEVICE_CLASS_CONNECTIVITY),
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities(
|
||||
[AmcrestBinarySensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
|
||||
@@ -36,10 +45,18 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type][0])
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
self._device_class = BINARY_SENSORS[sensor_type][1]
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return self._sensor_type != BINARY_SENSOR_ONLINE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -54,17 +71,39 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return device class."""
|
||||
return DEVICE_CLASS_MOTION
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._sensor_type == BINARY_SENSOR_ONLINE or self._api.available
|
||||
|
||||
def update(self):
|
||||
"""Update entity."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
|
||||
if not self.available:
|
||||
return
|
||||
_LOGGER.debug('Updating %s binary sensor', self._name)
|
||||
|
||||
try:
|
||||
self._state = self._api.is_motion_detected
|
||||
if self._sensor_type == BINARY_SENSOR_MOTION_DETECTED:
|
||||
self._state = self._api.is_motion_detected
|
||||
|
||||
elif self._sensor_type == BINARY_SENSOR_ONLINE:
|
||||
self._state = self._api.available
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not update %s binary sensor due to error: %s',
|
||||
self.name, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'update', self.name, 'binary sensor', error)
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to update signal."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
|
||||
self.async_on_demand_update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect from update signal."""
|
||||
self._unsub_dispatcher()
|
||||
|
@@ -1,7 +1,10 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from amcrest import AmcrestError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
@@ -14,11 +17,14 @@ from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST
|
||||
from .helpers import service_signal
|
||||
from .const import (
|
||||
CAMERA_WEB_SESSION_TIMEOUT, CAMERAS, DATA_AMCREST, DEVICES, SERVICE_UPDATE)
|
||||
from .helpers import log_update_error, service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
STREAM_SOURCE_LIST = [
|
||||
'snapshot',
|
||||
'mjpeg',
|
||||
@@ -76,7 +82,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities([
|
||||
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
|
||||
@@ -94,33 +100,36 @@ class AmcrestCam(Camera):
|
||||
self._stream_source = device.stream_source
|
||||
self._resolution = device.resolution
|
||||
self._token = self._auth = device.authentication
|
||||
self._control_light = device.control_light
|
||||
self._is_recording = False
|
||||
self._motion_detection_enabled = None
|
||||
self._brand = None
|
||||
self._model = None
|
||||
self._audio_enabled = None
|
||||
self._motion_recording_enabled = None
|
||||
self._color_bw = None
|
||||
self._rtsp_url = None
|
||||
self._snapshot_lock = asyncio.Lock()
|
||||
self._unsub_dispatcher = []
|
||||
self._update_succeeded = False
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
if not self.is_on:
|
||||
_LOGGER.error(
|
||||
'Attempt to take snaphot when %s camera is off', self.name)
|
||||
available = self.available
|
||||
if not available or not self.is_on:
|
||||
_LOGGER.warning(
|
||||
'Attempt to take snaphot when %s camera is %s', self.name,
|
||||
'offline' if not available else 'off')
|
||||
return None
|
||||
async with self._snapshot_lock:
|
||||
try:
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = await self.hass.async_add_executor_job(
|
||||
self._api.snapshot, self._resolution)
|
||||
self._api.snapshot)
|
||||
return response.data
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get image from %s camera due to error: %s',
|
||||
self.name, error)
|
||||
except (AmcrestError, HTTPError) as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'get image from', self.name, 'camera', error)
|
||||
return None
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
@@ -129,6 +138,12 @@ class AmcrestCam(Camera):
|
||||
if self._stream_source == 'snapshot':
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
if not self.available:
|
||||
_LOGGER.warning(
|
||||
'Attempt to stream %s when %s camera is offline',
|
||||
self._stream_source, self.name)
|
||||
return None
|
||||
|
||||
if self._stream_source == 'mjpeg':
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
@@ -143,7 +158,7 @@ class AmcrestCam(Camera):
|
||||
# streaming via ffmpeg
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
streaming_url = self._api.rtsp_url(typeno=self._resolution)
|
||||
streaming_url = self._rtsp_url
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
await stream.open_camera(
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
@@ -158,6 +173,14 @@ class AmcrestCam(Camera):
|
||||
|
||||
# Entity property overrides
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
@@ -176,6 +199,11 @@ class AmcrestCam(Camera):
|
||||
attr[_ATTR_COLOR_BW] = self._color_bw
|
||||
return attr
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._api.available
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
@@ -191,7 +219,7 @@ class AmcrestCam(Camera):
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
return self._brand
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
@@ -203,10 +231,9 @@ class AmcrestCam(Camera):
|
||||
"""Return the camera model."""
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def stream_source(self):
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._api.rtsp_url(typeno=self._resolution)
|
||||
return self._rtsp_url
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -215,6 +242,10 @@ class AmcrestCam(Camera):
|
||||
|
||||
# Other Entity method overrides
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to signals and add camera to list."""
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
@@ -222,28 +253,37 @@ class AmcrestCam(Camera):
|
||||
self.hass,
|
||||
service_signal(service, self.entity_id),
|
||||
getattr(self, params[1])))
|
||||
self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id)
|
||||
self._unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass, service_signal(SERVICE_UPDATE, self._name),
|
||||
self.async_on_demand_update))
|
||||
self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove camera from list and disconnect from signals."""
|
||||
self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id)
|
||||
self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id)
|
||||
for unsub_dispatcher in self._unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
|
||||
def update(self):
|
||||
"""Update entity status."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
_LOGGER.debug('Pulling data from %s camera', self.name)
|
||||
if self._model is None:
|
||||
try:
|
||||
self._model = self._api.device_type.split('=')[-1].strip()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera model due to error: %s',
|
||||
self.name, error)
|
||||
self._model = ''
|
||||
if not self.available or self._update_succeeded:
|
||||
if not self.available:
|
||||
self._update_succeeded = False
|
||||
return
|
||||
_LOGGER.debug('Updating %s camera', self.name)
|
||||
try:
|
||||
if self._brand is None:
|
||||
resp = self._api.vendor_information.strip()
|
||||
if resp.startswith('vendor='):
|
||||
self._brand = resp.split('=')[-1]
|
||||
else:
|
||||
self._brand = 'unknown'
|
||||
if self._model is None:
|
||||
resp = self._api.device_type.strip()
|
||||
if resp.startswith('type='):
|
||||
self._model = resp.split('=')[-1]
|
||||
else:
|
||||
self._model = 'unknown'
|
||||
self.is_streaming = self._api.video_enabled
|
||||
self._is_recording = self._api.record_mode == 'Manual'
|
||||
self._motion_detection_enabled = (
|
||||
@@ -252,10 +292,13 @@ class AmcrestCam(Camera):
|
||||
self._motion_recording_enabled = (
|
||||
self._api.is_record_on_motion_detection())
|
||||
self._color_bw = _CBW[self._api.day_night_color]
|
||||
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera attributes due to error: %s',
|
||||
self.name, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'get', self.name, 'camera attributes', error)
|
||||
self._update_succeeded = False
|
||||
else:
|
||||
self._update_succeeded = True
|
||||
|
||||
# Other Camera method overrides
|
||||
|
||||
@@ -323,8 +366,6 @@ class AmcrestCam(Camera):
|
||||
|
||||
def _enable_video_stream(self, enable):
|
||||
"""Enable or disable camera video stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# recording on if video stream is being turned off.
|
||||
@@ -333,17 +374,17 @@ class AmcrestCam(Camera):
|
||||
try:
|
||||
self._api.video_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera video stream due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, 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:
|
||||
self._enable_light(self._audio_enabled or self.is_streaming)
|
||||
|
||||
def _enable_recording(self, enable):
|
||||
"""Turn recording on or off."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# video stream off if recording is being turned on.
|
||||
@@ -354,88 +395,89 @@ class AmcrestCam(Camera):
|
||||
self._api.record_mode = rec_mode[
|
||||
'Manual' if enable else 'Automatic']
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'enable' if enable else 'disable', self.name,
|
||||
'camera recording', error)
|
||||
else:
|
||||
self._is_recording = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_motion_detection(self, enable):
|
||||
"""Enable or disable motion detection."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_detection = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion detection due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'enable' if enable else 'disable', self.name,
|
||||
'camera motion detection', error)
|
||||
else:
|
||||
self._motion_detection_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_audio(self, enable):
|
||||
"""Enable or disable audio stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.audio_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera audio stream due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, 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:
|
||||
self._enable_light(self._audio_enabled or self.is_streaming)
|
||||
|
||||
def _enable_light(self, enable):
|
||||
"""Enable or disable indicator light."""
|
||||
try:
|
||||
self._api.command(
|
||||
'configManager.cgi?action=setConfig&LightGlobal[0].Enable={}'
|
||||
.format(str(enable).lower()))
|
||||
except AmcrestError as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'enable' if enable else 'disable', self.name,
|
||||
'indicator light', error)
|
||||
|
||||
def _enable_motion_recording(self, enable):
|
||||
"""Enable or disable motion recording."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_recording = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, 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):
|
||||
"""Move camera position and zoom to preset."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.go_to_preset(
|
||||
action='start', preset_point_number=preset)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not move %s camera to preset %i due to error: %s',
|
||||
self.name, preset, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'move', self.name,
|
||||
'camera to preset {}'.format(preset), error)
|
||||
|
||||
def _set_color_bw(self, cbw):
|
||||
"""Set camera color mode."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.day_night_color = _CBW.index(cbw)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not set %s camera color mode to %s due to error: %s',
|
||||
self.name, cbw, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'set', self.name,
|
||||
'camera color mode to {}'.format(cbw), error)
|
||||
else:
|
||||
self._color_bw = cbw
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_tour(self, start):
|
||||
"""Start camera tour."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.tour(start=start)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera tour due to error: %s',
|
||||
'start' if start else 'stop', self.name, error)
|
||||
log_update_error(
|
||||
_LOGGER, 'start' if start else 'stop', self.name,
|
||||
'camera tour', error)
|
||||
|
@@ -1,7 +1,11 @@
|
||||
"""Constants for amcrest component."""
|
||||
DOMAIN = 'amcrest'
|
||||
DATA_AMCREST = DOMAIN
|
||||
CAMERAS = 'cameras'
|
||||
DEVICES = 'devices'
|
||||
|
||||
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
|
||||
CAMERA_WEB_SESSION_TIMEOUT = 10
|
||||
SENSOR_SCAN_INTERVAL_SECS = 10
|
||||
|
||||
SERVICE_UPDATE = 'update'
|
||||
|
@@ -2,9 +2,16 @@
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def service_signal(service, entity_id=None):
|
||||
"""Encode service and entity_id into signal."""
|
||||
def service_signal(service, ident=None):
|
||||
"""Encode service and identifier into signal."""
|
||||
signal = '{}_{}'.format(DOMAIN, service)
|
||||
if entity_id:
|
||||
signal += '_{}'.format(entity_id.replace('.', '_'))
|
||||
if ident:
|
||||
signal += '_{}'.format(ident.replace('.', '_'))
|
||||
return signal
|
||||
|
||||
|
||||
def log_update_error(logger, action, name, entity_type, error):
|
||||
"""Log an update error."""
|
||||
logger.error(
|
||||
'Could not %s %s %s due to error: %s',
|
||||
action, name, entity_type, error.__class__.__name__)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/components/amcrest",
|
||||
"requirements": [
|
||||
"amcrest==1.4.0"
|
||||
"amcrest==1.5.3"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg"
|
||||
|
@@ -2,21 +2,28 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from amcrest import AmcrestError
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SENSORS
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
|
||||
from .const import (
|
||||
DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE)
|
||||
from .helpers import log_update_error, service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSOR_PTZ_PRESET = 'ptz_preset'
|
||||
SENSOR_SDCARD = 'sdcard'
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
SENSOR_PTZ_PRESET: ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
SENSOR_SDCARD: ['SD Used', '%', 'mdi:sd'],
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +34,7 @@ async def async_setup_platform(
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities(
|
||||
[AmcrestSensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_SENSORS]],
|
||||
@@ -40,12 +47,14 @@ class AmcrestSensor(Entity):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize a sensor for Amcrest camera."""
|
||||
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
self._attrs = {}
|
||||
self._unit_of_measurement = SENSORS[sensor_type][1]
|
||||
self._icon = SENSORS[sensor_type][2]
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -72,28 +81,53 @@ class AmcrestSensor(Entity):
|
||||
"""Return the units of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._api.available
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Pulling data from %s sensor.", self._name)
|
||||
if not self.available:
|
||||
return
|
||||
_LOGGER.debug("Updating %s sensor", self._name)
|
||||
|
||||
if self._sensor_type == 'motion_detector':
|
||||
self._state = self._api.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._api.record_mode
|
||||
try:
|
||||
if self._sensor_type == SENSOR_MOTION_DETECTOR:
|
||||
self._state = self._api.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._api.record_mode
|
||||
|
||||
elif self._sensor_type == 'ptz_preset':
|
||||
self._state = self._api.ptz_presets_count
|
||||
elif self._sensor_type == SENSOR_PTZ_PRESET:
|
||||
self._state = self._api.ptz_presets_count
|
||||
|
||||
elif self._sensor_type == 'sdcard':
|
||||
storage = self._api.storage_all
|
||||
try:
|
||||
self._attrs['Total'] = '{:.2f} {}'.format(*storage['total'])
|
||||
except ValueError:
|
||||
self._attrs['Total'] = '{} {}'.format(*storage['total'])
|
||||
try:
|
||||
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
|
||||
except ValueError:
|
||||
self._attrs['Used'] = '{} {}'.format(*storage['used'])
|
||||
try:
|
||||
self._state = '{:.2f}'.format(storage['used_percent'])
|
||||
except ValueError:
|
||||
self._state = storage['used_percent']
|
||||
elif self._sensor_type == SENSOR_SDCARD:
|
||||
storage = self._api.storage_all
|
||||
try:
|
||||
self._attrs['Total'] = '{:.2f} {}'.format(
|
||||
*storage['total'])
|
||||
except ValueError:
|
||||
self._attrs['Total'] = '{} {}'.format(*storage['total'])
|
||||
try:
|
||||
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
|
||||
except ValueError:
|
||||
self._attrs['Used'] = '{} {}'.format(*storage['used'])
|
||||
try:
|
||||
self._state = '{:.2f}'.format(storage['used_percent'])
|
||||
except ValueError:
|
||||
self._state = storage['used_percent']
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, 'update', self.name, 'sensor', error)
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to update signal."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
|
||||
self.async_on_demand_update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect from update signal."""
|
||||
self._unsub_dispatcher()
|
||||
|
@@ -1,17 +1,23 @@
|
||||
"""Support for toggling Amcrest IP camera settings."""
|
||||
import logging
|
||||
|
||||
from amcrest import AmcrestError
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from .const import DATA_AMCREST
|
||||
from .const import DATA_AMCREST, DEVICES, SERVICE_UPDATE
|
||||
from .helpers import log_update_error, service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MOTION_DETECTION = 'motion_detection'
|
||||
MOTION_RECORDING = 'motion_recording'
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
MOTION_DETECTION: ['Motion Detection', 'mdi:run-fast'],
|
||||
MOTION_RECORDING: ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +28,7 @@ async def async_setup_platform(
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities(
|
||||
[AmcrestSwitch(name, device, setting)
|
||||
for setting in discovery_info[CONF_SWITCHES]],
|
||||
@@ -35,10 +41,12 @@ class AmcrestSwitch(ToggleEntity):
|
||||
def __init__(self, name, device, setting):
|
||||
"""Initialize the Amcrest switch."""
|
||||
self._name = '{} {}'.format(name, SWITCHES[setting][0])
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._setting = setting
|
||||
self._state = False
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -52,30 +60,63 @@ class AmcrestSwitch(ToggleEntity):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn setting on."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._api.motion_detection = 'true'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._api.motion_recording = 'true'
|
||||
if not self.available:
|
||||
return
|
||||
try:
|
||||
if self._setting == MOTION_DETECTION:
|
||||
self._api.motion_detection = 'true'
|
||||
elif self._setting == MOTION_RECORDING:
|
||||
self._api.motion_recording = 'true'
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, 'turn on', self.name, 'switch', error)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn setting off."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._api.motion_detection = 'false'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._api.motion_recording = 'false'
|
||||
if not self.available:
|
||||
return
|
||||
try:
|
||||
if self._setting == MOTION_DETECTION:
|
||||
self._api.motion_detection = 'false'
|
||||
elif self._setting == MOTION_RECORDING:
|
||||
self._api.motion_recording = 'false'
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, 'turn off', self.name, 'switch', error)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._api.available
|
||||
|
||||
def update(self):
|
||||
"""Update setting state."""
|
||||
_LOGGER.debug("Polling state for setting: %s ", self._name)
|
||||
if not self.available:
|
||||
return
|
||||
_LOGGER.debug("Updating %s switch", self._name)
|
||||
|
||||
if self._setting == 'motion_detection':
|
||||
detection = self._api.is_motion_detector_on()
|
||||
elif self._setting == 'motion_recording':
|
||||
detection = self._api.is_record_on_motion_detection()
|
||||
|
||||
self._state = detection
|
||||
try:
|
||||
if self._setting == MOTION_DETECTION:
|
||||
detection = self._api.is_motion_detector_on()
|
||||
elif self._setting == MOTION_RECORDING:
|
||||
detection = self._api.is_record_on_motion_detection()
|
||||
self._state = detection
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, 'update', self.name, 'switch', error)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon for the switch."""
|
||||
return self._icon
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to update signal."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
|
||||
self.async_on_demand_update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect from update signal."""
|
||||
self._unsub_dispatcher()
|
||||
|
@@ -233,7 +233,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Androidtv",
|
||||
"documentation": "https://www.home-assistant.io/components/androidtv",
|
||||
"requirements": [
|
||||
"androidtv==0.0.15"
|
||||
"androidtv==0.0.18"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@@ -90,20 +90,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
if CONF_ADB_SERVER_IP not in config:
|
||||
# Use "python-adb" (Python ADB implementation)
|
||||
adb_log = "using Python ADB implementation "
|
||||
if CONF_ADBKEY in config:
|
||||
aftv = setup(host, config[CONF_ADBKEY],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
|
||||
|
||||
else:
|
||||
aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = ""
|
||||
adb_log += "without adbkey authentication"
|
||||
else:
|
||||
# Use "pure-python-adb" (communicate with ADB server)
|
||||
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
|
||||
adb_server_port=config[CONF_ADB_SERVER_PORT],
|
||||
device_class=config[CONF_DEVICE_CLASS])
|
||||
adb_log = " using ADB server at {0}:{1}".format(
|
||||
adb_log = "using ADB server at {0}:{1}".format(
|
||||
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
|
||||
|
||||
if not aftv.available:
|
||||
@@ -117,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
else:
|
||||
device_name = 'Android TV / Fire TV device'
|
||||
|
||||
_LOGGER.warning("Could not connect to %s at %s%s",
|
||||
_LOGGER.warning("Could not connect to %s at %s %s",
|
||||
device_name, host, adb_log)
|
||||
raise PlatformNotReady
|
||||
|
||||
@@ -156,10 +157,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
for target_device in target_devices:
|
||||
output = target_device.adb_command(cmd)
|
||||
|
||||
# log the output if there is any
|
||||
if output and (not isinstance(output, str) or output.strip()):
|
||||
# log the output, if there is any
|
||||
if output:
|
||||
_LOGGER.info("Output of command '%s' from '%s': %s",
|
||||
cmd, target_device.entity_id, repr(output))
|
||||
cmd, target_device.entity_id, output)
|
||||
|
||||
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
|
||||
service_adb_command,
|
||||
@@ -224,6 +225,7 @@ class ADBDevice(MediaPlayerDevice):
|
||||
self.exceptions = (ConnectionResetError, RuntimeError)
|
||||
|
||||
# Property attributes
|
||||
self._adb_response = None
|
||||
self._available = self.aftv.available
|
||||
self._current_app = None
|
||||
self._state = None
|
||||
@@ -243,6 +245,11 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Return whether or not the ADB connection is valid."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide the last ADB command's response as an attribute."""
|
||||
return {'adb_response': self._adb_response}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
@@ -304,12 +311,24 @@ class ADBDevice(MediaPlayerDevice):
|
||||
"""Send an ADB command to an Android TV / Fire TV device."""
|
||||
key = self._keys.get(cmd)
|
||||
if key:
|
||||
return self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self.aftv.adb_shell('input keyevent {}'.format(key))
|
||||
self._adb_response = None
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
if cmd == 'GET_PROPERTIES':
|
||||
return self.aftv.get_properties_dict()
|
||||
self._adb_response = str(self.aftv.get_properties_dict())
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
return self.aftv.adb_shell(cmd)
|
||||
response = self.aftv.adb_shell(cmd)
|
||||
if isinstance(response, str) and response.strip():
|
||||
self._adb_response = response.strip()
|
||||
else:
|
||||
self._adb_response = None
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
return self._adb_response
|
||||
|
||||
|
||||
class AndroidTVDevice(ADBDevice):
|
||||
|
@@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
hass.async_create_task(device.async_update_ha_state())
|
||||
|
||||
avr = await anthemav.Connection.create(
|
||||
host=host, port=port, loop=hass.loop,
|
||||
host=host, port=port,
|
||||
update_callback=async_anthemav_update_callback)
|
||||
|
||||
device = AnthemAVR(avr, name)
|
||||
|
@@ -82,7 +82,7 @@ class APIEventStream(HomeAssistantView):
|
||||
raise Unauthorized()
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
to_write = asyncio.Queue()
|
||||
|
||||
restrict = request.query.get('restrict')
|
||||
if restrict:
|
||||
@@ -119,8 +119,7 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL):
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""APNS Notification platform."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -149,7 +148,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
self.devices = {}
|
||||
self.device_states = {}
|
||||
self.topic = topic
|
||||
if os.path.isfile(self.yaml_path):
|
||||
|
||||
try:
|
||||
self.devices = {
|
||||
str(key): ApnsDevice(
|
||||
str(key),
|
||||
@@ -160,6 +160,8 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
for (key, value) in
|
||||
load_yaml_config_file(self.yaml_path).items()
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tracking_ids = [
|
||||
device.full_tracking_device_id
|
||||
|
@@ -167,7 +167,7 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
|
1
homeassistant/components/aprs/__init__.py
Normal file
1
homeassistant/components/aprs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The APRS component."""
|
187
homeassistant/components/aprs/device_tracker.py
Normal file
187
homeassistant/components/aprs/device_tracker.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Support for APRS device tracking."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
CONF_HOST, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'aprs'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ALTITUDE = 'altitude'
|
||||
ATTR_COURSE = 'course'
|
||||
ATTR_COMMENT = 'comment'
|
||||
ATTR_FROM = 'from'
|
||||
ATTR_FORMAT = 'format'
|
||||
ATTR_POS_AMBIGUITY = 'posambiguity'
|
||||
ATTR_SPEED = 'speed'
|
||||
|
||||
CONF_CALLSIGNS = 'callsigns'
|
||||
|
||||
DEFAULT_HOST = 'rotate.aprs2.net'
|
||||
DEFAULT_PASSWORD = '-1'
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
|
||||
FILTER_PORT = 14580
|
||||
|
||||
MSG_FORMATS = ['compressed', 'uncompressed', 'mic-e']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CALLSIGNS): cv.ensure_list,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD,
|
||||
default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST,
|
||||
default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT,
|
||||
default=DEFAULT_TIMEOUT): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
def make_filter(callsigns: list) -> str:
|
||||
"""Make a server-side filter from a list of callsigns."""
|
||||
return ' '.join('b/{0}'.format(cs.upper()) for cs in callsigns)
|
||||
|
||||
|
||||
def gps_accuracy(gps, posambiguity: int) -> int:
|
||||
"""Calculate the GPS accuracy based on APRS posambiguity."""
|
||||
import geopy.distance
|
||||
|
||||
pos_a_map = {0: 0,
|
||||
1: 1 / 600,
|
||||
2: 1 / 60,
|
||||
3: 1 / 6,
|
||||
4: 1}
|
||||
if posambiguity in pos_a_map:
|
||||
degrees = pos_a_map[posambiguity]
|
||||
|
||||
gps2 = (gps[0], gps[1] + degrees)
|
||||
dist_m = geopy.distance.distance(gps, gps2).m
|
||||
|
||||
accuracy = round(dist_m)
|
||||
else:
|
||||
message = "APRS position ambiguity must be 0-4, not '{0}'.".format(
|
||||
posambiguity)
|
||||
raise ValueError(message)
|
||||
|
||||
return accuracy
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the APRS tracker."""
|
||||
callsigns = config.get(CONF_CALLSIGNS)
|
||||
server_filter = make_filter(callsigns)
|
||||
|
||||
callsign = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
aprs_listener = AprsListenerThread(
|
||||
callsign, password, host, server_filter, see)
|
||||
|
||||
def aprs_disconnect(event):
|
||||
"""Stop the APRS connection."""
|
||||
aprs_listener.stop()
|
||||
|
||||
aprs_listener.start()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect)
|
||||
|
||||
if not aprs_listener.start_event.wait(timeout):
|
||||
_LOGGER.error("Timeout waiting for APRS to connect.")
|
||||
return
|
||||
|
||||
if not aprs_listener.start_success:
|
||||
_LOGGER.error(aprs_listener.start_message)
|
||||
return
|
||||
|
||||
_LOGGER.debug(aprs_listener.start_message)
|
||||
return True
|
||||
|
||||
|
||||
class AprsListenerThread(threading.Thread):
|
||||
"""APRS message listener."""
|
||||
|
||||
def __init__(self, callsign: str, password: str, host: str,
|
||||
server_filter: str, see):
|
||||
"""Initialize the class."""
|
||||
super().__init__()
|
||||
|
||||
import aprslib
|
||||
|
||||
self.callsign = callsign
|
||||
self.host = host
|
||||
self.start_event = threading.Event()
|
||||
self.see = see
|
||||
self.server_filter = server_filter
|
||||
self.start_message = ""
|
||||
self.start_success = False
|
||||
|
||||
self.ais = aprslib.IS(
|
||||
self.callsign, passwd=password, host=self.host, port=FILTER_PORT)
|
||||
|
||||
def start_complete(self, success: bool, message: str):
|
||||
"""Complete startup process."""
|
||||
self.start_message = message
|
||||
self.start_success = success
|
||||
self.start_event.set()
|
||||
|
||||
def run(self):
|
||||
"""Connect to APRS and listen for data."""
|
||||
self.ais.set_filter(self.server_filter)
|
||||
from aprslib import ConnectionError as AprsConnectionError
|
||||
from aprslib import LoginError
|
||||
|
||||
try:
|
||||
_LOGGER.info("Opening connection to %s with callsign %s.",
|
||||
self.host, self.callsign)
|
||||
self.ais.connect()
|
||||
self.start_complete(
|
||||
True,
|
||||
"Connected to {0} with callsign {1}.".format(
|
||||
self.host, self.callsign))
|
||||
self.ais.consumer(callback=self.rx_msg, immortal=True)
|
||||
except (AprsConnectionError, LoginError) as err:
|
||||
self.start_complete(False, str(err))
|
||||
except OSError:
|
||||
_LOGGER.info("Closing connection to %s with callsign %s.",
|
||||
self.host, self.callsign)
|
||||
|
||||
def stop(self):
|
||||
"""Close the connection to the APRS network."""
|
||||
self.ais.close()
|
||||
|
||||
def rx_msg(self, msg: dict):
|
||||
"""Receive message and process if position."""
|
||||
_LOGGER.debug("APRS message received: %s", str(msg))
|
||||
if msg[ATTR_FORMAT] in MSG_FORMATS:
|
||||
dev_id = slugify(msg[ATTR_FROM])
|
||||
lat = msg[ATTR_LATITUDE]
|
||||
lon = msg[ATTR_LONGITUDE]
|
||||
|
||||
attrs = {}
|
||||
if ATTR_POS_AMBIGUITY in msg:
|
||||
pos_amb = msg[ATTR_POS_AMBIGUITY]
|
||||
try:
|
||||
attrs[ATTR_GPS_ACCURACY] = gps_accuracy((lat, lon),
|
||||
pos_amb)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"APRS message contained invalid posambiguity: %s",
|
||||
str(pos_amb))
|
||||
for attr in [ATTR_ALTITUDE,
|
||||
ATTR_COMMENT,
|
||||
ATTR_COURSE,
|
||||
ATTR_SPEED]:
|
||||
if attr in msg:
|
||||
attrs[attr] = msg[attr]
|
||||
|
||||
self.see(dev_id=dev_id, gps=(lat, lon), attributes=attrs)
|
11
homeassistant/components/aprs/manifest.json
Normal file
11
homeassistant/components/aprs/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "aprs",
|
||||
"name": "APRS",
|
||||
"documentation": "https://www.home-assistant.io/components/aprs",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@PhilRW"],
|
||||
"requirements": [
|
||||
"aprslib==0.6.46",
|
||||
"geopy==1.19.0"
|
||||
]
|
||||
}
|
5
homeassistant/components/arcam_fmj/.translations/ca.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/ca.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
5
homeassistant/components/arcam_fmj/.translations/en.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/en.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
5
homeassistant/components/arcam_fmj/.translations/ko.json
Normal file
5
homeassistant/components/arcam_fmj/.translations/ko.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
17
homeassistant/components/arcam_fmj/.translations/nl.json
Normal file
17
homeassistant/components/arcam_fmj/.translations/nl.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"error": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"step": {
|
||||
"one": "Een",
|
||||
"other": "Ander"
|
||||
},
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
23
homeassistant/components/arcam_fmj/.translations/pl.json
Normal file
23
homeassistant/components/arcam_fmj/.translations/pl.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"error": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"step": {
|
||||
"few": "kilka",
|
||||
"many": "wiele",
|
||||
"one": "jeden",
|
||||
"other": "inne"
|
||||
},
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Arcam FMJ"
|
||||
}
|
||||
}
|
176
homeassistant/components/arcam_fmj/__init__.py
Normal file
176
homeassistant/components/arcam_fmj/__init__.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Arcam component."""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
import async_timeout
|
||||
from arcam.fmj.client import Client
|
||||
from arcam.fmj import ConnectionFailed
|
||||
|
||||
from homeassistant import config_entries
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_ZONE,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DOMAIN_DATA_ENTRIES,
|
||||
DOMAIN_DATA_CONFIG,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
SIGNAL_CLIENT_DATA,
|
||||
SIGNAL_CLIENT_STARTED,
|
||||
SIGNAL_CLIENT_STOPPED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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] = "{} ({}:{}) - {}".format(
|
||||
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)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the component."""
|
||||
hass.data[DOMAIN_DATA_ENTRIES] = {}
|
||||
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],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Set up an access point from a config entry."""
|
||||
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
|
||||
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],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
|
||||
"client": client,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
asyncio.ensure_future(
|
||||
_run_client(hass, client, config[CONF_SCAN_INTERVAL])
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _run_client(hass, client, interval):
|
||||
task = asyncio.Task.current_task()
|
||||
run = True
|
||||
|
||||
async def _stop(_):
|
||||
nonlocal run
|
||||
run = False
|
||||
task.cancel()
|
||||
await task
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop)
|
||||
|
||||
def _listen(_):
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_DATA, client.host
|
||||
)
|
||||
|
||||
while run:
|
||||
try:
|
||||
with async_timeout.timeout(interval):
|
||||
await client.start()
|
||||
|
||||
_LOGGER.debug("Client connected %s", client.host)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_STARTED, client.host
|
||||
)
|
||||
|
||||
try:
|
||||
with client.listen(_listen):
|
||||
await client.process()
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
_LOGGER.debug("Client disconnected %s", client.host)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_CLIENT_STOPPED, client.host
|
||||
)
|
||||
|
||||
except ConnectionFailed:
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user