mirror of
https://github.com/home-assistant/core.git
synced 2026-02-03 22:05:35 +01:00
Compare commits
1048 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0871d6c9c6 | ||
|
|
6cbfc63311 | ||
|
|
2886b217ab | ||
|
|
fafc68673a | ||
|
|
1990df63aa | ||
|
|
e39f0f3e25 | ||
|
|
1f5e2fa3ce | ||
|
|
204dd77404 | ||
|
|
5598f05dee | ||
|
|
01b6830fd2 | ||
|
|
c1d0ac7b9d | ||
|
|
dce667fa07 | ||
|
|
a78361341e | ||
|
|
4dbfafa8ca | ||
|
|
5f37852695 | ||
|
|
5fe8a43e36 | ||
|
|
760b62e068 | ||
|
|
9205334235 | ||
|
|
ca4c6ffe8d | ||
|
|
b47b555c4f | ||
|
|
5d2f97de74 | ||
|
|
87712b9fa5 | ||
|
|
510d6d7874 | ||
|
|
8830054fad | ||
|
|
327fe63047 | ||
|
|
0f5c9b4af3 | ||
|
|
d699a550c8 | ||
|
|
f71d4312e2 | ||
|
|
ec777a802c | ||
|
|
82cad58b8d | ||
|
|
34231383ec | ||
|
|
75ec855822 | ||
|
|
2c5080e382 | ||
|
|
48e9742658 | ||
|
|
14b62120fd | ||
|
|
4a8149627e | ||
|
|
9c85ba5b66 | ||
|
|
fb0cb43261 | ||
|
|
23722dc291 | ||
|
|
9b096322e1 | ||
|
|
7f169e97ca | ||
|
|
1761a71338 | ||
|
|
198432f222 | ||
|
|
a868685ac9 | ||
|
|
d4cab60343 | ||
|
|
da12ceae5b | ||
|
|
79b10612aa | ||
|
|
d5edbb424a | ||
|
|
d527e2c926 | ||
|
|
b899dd59c5 | ||
|
|
8f928982e0 | ||
|
|
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 | ||
|
|
7818c98c67 | ||
|
|
e12222697c | ||
|
|
58f28f177d | ||
|
|
6030e419c5 | ||
|
|
8d2a784831 | ||
|
|
5dc841ecae | ||
|
|
edf34eea94 | ||
|
|
a303f67d3b | ||
|
|
1b5f526e09 | ||
|
|
03a0a3572b | ||
|
|
297d24c5b0 | ||
|
|
c8cf06b8b7 | ||
|
|
49d6d7c656 | ||
|
|
96fd874090 | ||
|
|
c9703872e2 | ||
|
|
2f5d7d4522 | ||
|
|
7716e8fb68 | ||
|
|
c2fc8a0d61 | ||
|
|
9be384690a | ||
|
|
692eeb3687 | ||
|
|
213c91ae73 | ||
|
|
36b1a89f93 | ||
|
|
584bfbaa76 | ||
|
|
0f140751b2 | ||
|
|
6b359c95da | ||
|
|
1fec64a1b3 | ||
|
|
70ed58a78d | ||
|
|
6aa9844f8f | ||
|
|
7a4238095d | ||
|
|
177594f02c | ||
|
|
cf89f45697 | ||
|
|
2dc78e6f0c | ||
|
|
9da74dda43 | ||
|
|
18149dcb8c | ||
|
|
3f841a36a5 | ||
|
|
80ae02cc49 | ||
|
|
421b2962c6 | ||
|
|
bde5a9ef01 | ||
|
|
b79886ad85 | ||
|
|
94a2fd542e | ||
|
|
6fa8556033 | ||
|
|
19cfa8cf22 | ||
|
|
a859997190 | ||
|
|
6f9860b25e | ||
|
|
f083abbed1 | ||
|
|
4dd8423b9b | ||
|
|
8ba2ab567e | ||
|
|
3d821b9148 | ||
|
|
04720175b9 | ||
|
|
93ad7b2e45 | ||
|
|
128ce589e1 | ||
|
|
9b21774392 | ||
|
|
eaf4a75402 | ||
|
|
a1a6d4a631 | ||
|
|
de1fd5a7fa | ||
|
|
9b12dd66e4 | ||
|
|
0d96095646 | ||
|
|
45085dd97f | ||
|
|
b2a1204bc5 | ||
|
|
990a9e80a2 | ||
|
|
0ffcc197d4 | ||
|
|
1a051f038d | ||
|
|
1e22c8daca | ||
|
|
b8cbd39985 | ||
|
|
3508622e3b | ||
|
|
b8f6d824fd | ||
|
|
e687848152 | ||
|
|
2a9fd9ae26 | ||
|
|
3ec4070d8c | ||
|
|
6f8038992c | ||
|
|
5c9a58f3e6 | ||
|
|
d34214ad32 | ||
|
|
2b7021407c | ||
|
|
03cd4480df | ||
|
|
910825580e | ||
|
|
6d40980de1 | ||
|
|
d4f79fc88a | ||
|
|
2d724f5cc9 | ||
|
|
c8d479e594 | ||
|
|
34f6245e74 | ||
|
|
e9ea5c2ccb | ||
|
|
4347a0f6b7 | ||
|
|
5888e32360 | ||
|
|
4214a354a7 | ||
|
|
61ed604fda | ||
|
|
f17259c705 | ||
|
|
d5533cc10f | ||
|
|
e6c5cc92d1 | ||
|
|
0caa27094e | ||
|
|
01578f78f1 | ||
|
|
369afd7ddd | ||
|
|
281445917b | ||
|
|
df6846344d | ||
|
|
05960fa29c | ||
|
|
068749bcbe | ||
|
|
f21f32778f | ||
|
|
8ef3c6d4d3 | ||
|
|
c7a78ed522 | ||
|
|
45adb5c9c7 | ||
|
|
4004867eda | ||
|
|
118d3bc11c | ||
|
|
0e9d71f232 | ||
|
|
b552fbe312 | ||
|
|
07b7d7b074 | ||
|
|
fb7343ecd2 | ||
|
|
e51925fc58 | ||
|
|
d507adf13d | ||
|
|
c5230f7585 | ||
|
|
cc13713abd | ||
|
|
f019e2a204 | ||
|
|
07126266dd | ||
|
|
c26af22edd | ||
|
|
c384adeef4 | ||
|
|
7f0953766b | ||
|
|
ce1974b014 | ||
|
|
8822f0140d | ||
|
|
45c041884a | ||
|
|
1ecb3de643 | ||
|
|
f0f6787bf9 | ||
|
|
3e788aa1d6 | ||
|
|
f4016b4aad | ||
|
|
07ee3b2eb9 | ||
|
|
6e7a7ba4a0 | ||
|
|
7181d639fd | ||
|
|
0f174250cc | ||
|
|
16a27c3f2d | ||
|
|
b253c7499d | ||
|
|
264e70922b | ||
|
|
482cb0146a | ||
|
|
02d8731a61 | ||
|
|
102beaa044 | ||
|
|
6611d585e6 | ||
|
|
f386088def | ||
|
|
c07c557298 | ||
|
|
1f551e5f6f | ||
|
|
b60c815cde | ||
|
|
074142400a | ||
|
|
d8a219fe0e | ||
|
|
a0230482bd | ||
|
|
b9a72034f9 | ||
|
|
bf649e373c | ||
|
|
73aadbe8bc | ||
|
|
e2afeca4fd | ||
|
|
0dfa5f9ffd | ||
|
|
b331b081f0 | ||
|
|
cf03e42773 | ||
|
|
f87209605b | ||
|
|
3e59e7f347 | ||
|
|
3dd1d3c418 | ||
|
|
8328ea6bd7 | ||
|
|
9c1bbd1d9d | ||
|
|
8500244f8c | ||
|
|
2efc1de349 | ||
|
|
4796d674e6 | ||
|
|
4fe0cd76f8 | ||
|
|
c0f9ccfdbb | ||
|
|
b9fda078a4 | ||
|
|
e24d56aa5b | ||
|
|
8da600adff | ||
|
|
9712bbc91c | ||
|
|
fca0891320 | ||
|
|
1d2c5cb53c | ||
|
|
c309bd9ff0 | ||
|
|
2f416b15c5 | ||
|
|
85cd4ad022 | ||
|
|
c8690865ec | ||
|
|
8d8d2b6de9 | ||
|
|
97b5a38cb1 | ||
|
|
8fc30569a9 | ||
|
|
0702407fac | ||
|
|
6e34015420 | ||
|
|
df9a9a1fec | ||
|
|
9761c504eb | ||
|
|
b30afde8ab | ||
|
|
6130831a43 | ||
|
|
3338f5c9b4 | ||
|
|
592e99947d | ||
|
|
7331eb1f71 | ||
|
|
f434e24252 | ||
|
|
b79d71065c | ||
|
|
0a75a2c080 | ||
|
|
41357965de | ||
|
|
1e6babe796 | ||
|
|
04b680d9d0 | ||
|
|
5267635d2c | ||
|
|
a6b898d702 | ||
|
|
5c413eb497 | ||
|
|
21575938ef | ||
|
|
a7ef1eabb0 | ||
|
|
6124a6f7e5 | ||
|
|
d4ae73ce38 | ||
|
|
2fbf29cda6 | ||
|
|
3c5abcc71a | ||
|
|
447440adc3 | ||
|
|
daa1d103d4 | ||
|
|
58cde6b497 | ||
|
|
741d0fd09b | ||
|
|
bc9548fdaf | ||
|
|
35e9505ad5 | ||
|
|
38aa7d2c95 | ||
|
|
55a7ea6cc5 | ||
|
|
04bca7be6b | ||
|
|
8180797d2f | ||
|
|
0fe573ecc0 | ||
|
|
185595c113 | ||
|
|
ffdf48b15a | ||
|
|
fa4264be3f | ||
|
|
c7a34d0222 | ||
|
|
e054d68565 | ||
|
|
f3925b7ede | ||
|
|
d1e44e35df | ||
|
|
0fe21f2015 | ||
|
|
75abfd49ef | ||
|
|
f0c582ebbd | ||
|
|
7ff77936ad | ||
|
|
44d0d0624b | ||
|
|
a8c7804db2 | ||
|
|
beb678e259 | ||
|
|
d9d5c91adc | ||
|
|
19aee50bbc | ||
|
|
bb6300efe3 | ||
|
|
dd53434742 | ||
|
|
db2904624a | ||
|
|
d3cbd5b5e4 | ||
|
|
f9205d0ccc | ||
|
|
127cc5f942 | ||
|
|
ab60235811 | ||
|
|
7faa061b29 | ||
|
|
c7d49a0c6a | ||
|
|
ac731a817a | ||
|
|
f0a34ddf46 | ||
|
|
918ce74b26 | ||
|
|
e8f496c016 | ||
|
|
9b57075c3b | ||
|
|
09cd302b46 | ||
|
|
bf25b74bf1 | ||
|
|
3269da16f6 | ||
|
|
0a428868fe | ||
|
|
8cfc316e06 | ||
|
|
31e3c563b5 | ||
|
|
f28ca34307 | ||
|
|
09296b4fd4 | ||
|
|
0b9302ac3d | ||
|
|
5b9d01139d | ||
|
|
581b16e9fa | ||
|
|
1c4367e5a9 | ||
|
|
b0843f4a38 | ||
|
|
24060e0fb5 | ||
|
|
5ded0dd3fa | ||
|
|
09012e7baa | ||
|
|
75a2c057f2 | ||
|
|
603e2cd961 | ||
|
|
cfaaae661a | ||
|
|
41f0066e76 | ||
|
|
407e0c58f9 | ||
|
|
d71424f285 | ||
|
|
6a6a999833 | ||
|
|
7612703092 | ||
|
|
5d5f073cff | ||
|
|
1d70005b01 | ||
|
|
b4e2a0ef84 | ||
|
|
2aee31ec6a | ||
|
|
8d775caaac | ||
|
|
4c4f0e38d4 | ||
|
|
5e3e730496 | ||
|
|
84f778d23c | ||
|
|
75f53b2799 | ||
|
|
e08f2ad18d | ||
|
|
5aa9a1a7c2 | ||
|
|
5e045f3df2 | ||
|
|
471a26bde1 | ||
|
|
2245ee98e3 | ||
|
|
0ecf152153 | ||
|
|
5529bcc114 | ||
|
|
b4a7980084 | ||
|
|
0f49a9cb7b | ||
|
|
2f45a7e3b9 | ||
|
|
b60c7ce479 | ||
|
|
41d9bd42af | ||
|
|
2fecc7d5a4 | ||
|
|
687bbce900 | ||
|
|
54c34bb224 | ||
|
|
37badbbf09 | ||
|
|
b09f5b6743 | ||
|
|
2dbe6d3289 | ||
|
|
300d1f44a6 | ||
|
|
7458f1f6ef | ||
|
|
26bf1b2173 | ||
|
|
f1b2622d78 | ||
|
|
5efe089699 | ||
|
|
b6a13262da | ||
|
|
148860587c | ||
|
|
6be798bffc | ||
|
|
1dbfa8f3be | ||
|
|
96fb311f6b | ||
|
|
bdc95e76d0 | ||
|
|
c174b83f54 | ||
|
|
bf050adcf3 | ||
|
|
c2e7445271 | ||
|
|
f25183ba30 | ||
|
|
ee107755f8 | ||
|
|
081a0290ba | ||
|
|
95ed8fb245 | ||
|
|
0d4858e296 | ||
|
|
f6a6be9a22 | ||
|
|
065b077369 | ||
|
|
1ec08ce243 | ||
|
|
46c955a501 | ||
|
|
c1429f5d80 | ||
|
|
ed16681b8e | ||
|
|
1ab03d9e15 | ||
|
|
ffcaeb4ef1 | ||
|
|
dd1e352d1d | ||
|
|
3bfb5b119a | ||
|
|
a269603e3b | ||
|
|
d6f6273ac2 | ||
|
|
61ea6256c6 | ||
|
|
08c36e0089 | ||
|
|
8fe95f4bab | ||
|
|
606dbb85d2 | ||
|
|
b84ba93c42 | ||
|
|
5dbf58d67f | ||
|
|
d038d2426b | ||
|
|
b5725f8f19 | ||
|
|
eefb9406c2 | ||
|
|
c229a314c6 | ||
|
|
7d5c1ede72 | ||
|
|
7a6acca6bb | ||
|
|
9d67c9feb6 | ||
|
|
cef7ce11ad | ||
|
|
e182b95921 | ||
|
|
d2e0c6dbc2 | ||
|
|
39932d132d | ||
|
|
7e8f2d72b6 | ||
|
|
4816a24b3c | ||
|
|
3d91d76d3d | ||
|
|
5376e15286 | ||
|
|
86b017e2f0 | ||
|
|
c216ac7260 | ||
|
|
de6fdb09f4 | ||
|
|
e3e7fb5ff6 | ||
|
|
6fb5b8467b | ||
|
|
24766df179 | ||
|
|
ec9db7f9a2 | ||
|
|
0d796a0fb9 | ||
|
|
96735e41af | ||
|
|
fef1dc8c54 | ||
|
|
ef5ca63bf0 | ||
|
|
8ae2ce2299 | ||
|
|
700b8b2d0c | ||
|
|
d218ba98e7 | ||
|
|
806aba4a1a | ||
|
|
1128cf576f | ||
|
|
febdb72fb2 | ||
|
|
3c0146d382 | ||
|
|
9e76293141 | ||
|
|
6149c2877d | ||
|
|
2efe607b78 | ||
|
|
9fc271d178 | ||
|
|
d53a00d054 | ||
|
|
62fcb1895e | ||
|
|
843bad83fa | ||
|
|
e850ccb82c | ||
|
|
f4e7364651 | ||
|
|
82ff5cbe0f | ||
|
|
e11e6e1b04 | ||
|
|
2863ac1068 | ||
|
|
3d04856cbd | ||
|
|
7c55b9f087 | ||
|
|
6681605c34 | ||
|
|
95bbea20a8 | ||
|
|
662375bdd7 | ||
|
|
aa26f90420 | ||
|
|
c61b6cf616 | ||
|
|
16d8e92b06 | ||
|
|
68d3e624e6 | ||
|
|
d505f1c5f2 | ||
|
|
b252d8e2cd | ||
|
|
c040f7abc0 | ||
|
|
2871a650f6 | ||
|
|
5b0ee473b6 | ||
|
|
00d26b3049 | ||
|
|
06f76e8e97 | ||
|
|
d5bd8b9405 | ||
|
|
34c03109e5 | ||
|
|
df3ceb8d87 | ||
|
|
f514d44224 | ||
|
|
7fb0055a92 | ||
|
|
f81eeded90 | ||
|
|
6df31da180 | ||
|
|
364f5c8c02 | ||
|
|
85ac85c959 | ||
|
|
a2565ad3b4 | ||
|
|
ddb5ff3b71 | ||
|
|
72bbe2203e | ||
|
|
2a720efbd4 | ||
|
|
baeb3cddc6 | ||
|
|
ee88433fb1 | ||
|
|
845d81bdae | ||
|
|
d0f9595ad9 | ||
|
|
9007e17c3e | ||
|
|
e85af58e43 | ||
|
|
0c90bfb936 | ||
|
|
8daba68dc1 | ||
|
|
e3981b6498 | ||
|
|
6d31d56c03 | ||
|
|
a89c7f8feb | ||
|
|
357631d659 | ||
|
|
3b0660ae89 | ||
|
|
a8632480ff | ||
|
|
80653824d9 | ||
|
|
b3c7142030 | ||
|
|
df32830f17 | ||
|
|
b697bb7a26 | ||
|
|
ef28e2cc2a | ||
|
|
ff047e1cd1 | ||
|
|
8741a20191 | ||
|
|
d0c6f0b710 | ||
|
|
15c1213928 | ||
|
|
f618e7253a | ||
|
|
5df57bbda5 | ||
|
|
4433ad0a06 | ||
|
|
f18a49ce97 | ||
|
|
23cb053e82 | ||
|
|
52eb9e50aa | ||
|
|
35da3f053c | ||
|
|
1a4a9532dd | ||
|
|
2bb772bbdc | ||
|
|
ad5d4bbf51 | ||
|
|
9c9b25d4b9 | ||
|
|
89037b367b | ||
|
|
a3c3c41faa | ||
|
|
7eebf4631d | ||
|
|
decaabeb4a | ||
|
|
df02879c51 | ||
|
|
dbb49afb3e | ||
|
|
a3ecde01ee | ||
|
|
e2ed2ecdc0 | ||
|
|
1e0bc97f56 | ||
|
|
eebb452fb5 | ||
|
|
2c42e1a5cb | ||
|
|
28c411c742 | ||
|
|
9d8d8afa82 | ||
|
|
31e514ec15 | ||
|
|
73a7d5e6f4 | ||
|
|
f584878204 | ||
|
|
0533f56fe3 | ||
|
|
416af5cf57 | ||
|
|
557211240e | ||
|
|
13e0691c90 | ||
|
|
0e429cca33 | ||
|
|
887e1cd8e3 | ||
|
|
c899e2a662 | ||
|
|
e7054e0fd2 | ||
|
|
9cf9be8850 | ||
|
|
b1b269b302 | ||
|
|
6e300bd438 | ||
|
|
21a194f9d8 | ||
|
|
b3a8b0056b | ||
|
|
b2a7699cdf | ||
|
|
3e443d253c | ||
|
|
6a7bd19a5a | ||
|
|
dbe0ba87a3 | ||
|
|
bea7e2a7fa | ||
|
|
eac2388d49 | ||
|
|
7a84cfb0be | ||
|
|
b0ce3dc683 | ||
|
|
70ba5eb0ef | ||
|
|
1761b25879 | ||
|
|
c2b4e24372 | ||
|
|
5e363d124e | ||
|
|
a52f96b23a | ||
|
|
620c6a22ac | ||
|
|
e1d1f21a74 | ||
|
|
66b2ed930c | ||
|
|
33b8241d26 | ||
|
|
37cd711c96 | ||
|
|
0eb8c77889 | ||
|
|
4be30f7c88 | ||
|
|
70c5bd4316 | ||
|
|
daf2f30822 | ||
|
|
fda483f482 | ||
|
|
c2cce13e2a | ||
|
|
38d23ba0af | ||
|
|
5e1338a9e4 | ||
|
|
4ac9a2e9de | ||
|
|
f57191e8dd | ||
|
|
11fb4866a8 | ||
|
|
d9fb3c8c28 | ||
|
|
df475cb797 | ||
|
|
f588fef3b4 | ||
|
|
6e4083d7f4 | ||
|
|
25a5bd32e2 | ||
|
|
3665e87800 | ||
|
|
fa0d538358 | ||
|
|
c508d5905b | ||
|
|
dc2cb62265 | ||
|
|
d0c3a8ecaf | ||
|
|
4a2a130bfa | ||
|
|
ce8ec3acb1 | ||
|
|
474ac8b09e | ||
|
|
77244eab1e | ||
|
|
6bb4199824 | ||
|
|
f6349a6cf4 | ||
|
|
fa73b8e37a | ||
|
|
0afa01609c | ||
|
|
723d00d33a |
@@ -57,7 +57,7 @@ commands:
|
||||
<<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<</ parameters.all>>
|
||||
<<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<</ parameters.test>>
|
||||
<<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<</ parameters.test_all>>
|
||||
no_output_timeout: 15m
|
||||
no_output_timeout: 15m
|
||||
- save_cache:
|
||||
paths:
|
||||
- ./venv
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
name: run static check
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
flake8
|
||||
flake8 homeassistant tests script
|
||||
|
||||
- run:
|
||||
name: run static type check
|
||||
|
||||
@@ -13,3 +13,4 @@ coverage:
|
||||
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
|
||||
comment:
|
||||
require_changes: yes
|
||||
branches: master
|
||||
|
||||
39
.coveragerc
39
.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
|
||||
@@ -22,6 +26,7 @@ omit =
|
||||
homeassistant/components/alarmdotcom/alarm_control_panel.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/tts.py
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
homeassistant/components/ampio/*
|
||||
@@ -46,12 +51,14 @@ omit =
|
||||
homeassistant/components/august/*
|
||||
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
|
||||
homeassistant/components/bbox/sensor.py
|
||||
homeassistant/components/bh1750/sensor.py
|
||||
homeassistant/components/bitcoin/sensor.py
|
||||
homeassistant/components/bizkaibus/sensor.py
|
||||
homeassistant/components/blink/*
|
||||
homeassistant/components/blinksticklight/light.py
|
||||
homeassistant/components/blinkt/light.py
|
||||
@@ -150,6 +157,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/*
|
||||
@@ -158,6 +166,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
|
||||
@@ -169,10 +178,12 @@ 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
|
||||
homeassistant/components/esphome/switch.py
|
||||
homeassistant/components/essent/sensor.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
homeassistant/components/everlights/light.py
|
||||
@@ -218,6 +229,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/*
|
||||
@@ -247,7 +259,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
|
||||
@@ -275,9 +286,11 @@ omit =
|
||||
homeassistant/components/imap_email_content/sensor.py
|
||||
homeassistant/components/influxdb/sensor.py
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/incomfort/*
|
||||
homeassistant/components/ios/*
|
||||
homeassistant/components/iota/*
|
||||
homeassistant/components/iperf3/*
|
||||
homeassistant/components/iqvia/*
|
||||
homeassistant/components/irish_rail_transport/sensor.py
|
||||
homeassistant/components/iss/binary_sensor.py
|
||||
homeassistant/components/isy994/*
|
||||
@@ -306,6 +319,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
|
||||
@@ -339,11 +353,13 @@ 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
|
||||
homeassistant/components/met/weather.py
|
||||
homeassistant/components/meteo_france/*
|
||||
homeassistant/components/meteoalarm/*
|
||||
homeassistant/components/metoffice/sensor.py
|
||||
homeassistant/components/metoffice/weather.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
@@ -418,6 +434,7 @@ omit =
|
||||
homeassistant/components/openweathermap/sensor.py
|
||||
homeassistant/components/openweathermap/weather.py
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/orangepi_gpio/*
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
@@ -435,12 +452,12 @@ 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/*
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
homeassistant/components/point/*
|
||||
homeassistant/components/pollen/sensor.py
|
||||
homeassistant/components/postnl/sensor.py
|
||||
homeassistant/components/prezzibenzina/sensor.py
|
||||
homeassistant/components/proliphix/climate.py
|
||||
@@ -449,6 +466,7 @@ omit =
|
||||
homeassistant/components/proxy/camera.py
|
||||
homeassistant/components/ps4/__init__.py
|
||||
homeassistant/components/ps4/media_player.py
|
||||
homeassistant/components/ptvsd/*
|
||||
homeassistant/components/pulseaudio_loopback/switch.py
|
||||
homeassistant/components/pushbullet/notify.py
|
||||
homeassistant/components/pushbullet/sensor.py
|
||||
@@ -480,6 +498,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
|
||||
@@ -532,14 +553,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/device_tracker.py
|
||||
homeassistant/components/snmp/sensor.py
|
||||
homeassistant/components/snmp/switch.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
|
||||
@@ -555,12 +579,14 @@ 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
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology/camera.py
|
||||
@@ -607,6 +633,7 @@ 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
|
||||
@@ -638,6 +665,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/*
|
||||
@@ -645,6 +673,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/*
|
||||
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,7 +7,7 @@
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
|
||||
**Pull request with documentation for [home-assistant.io](https://github.com/home-assistant/home-assistant.io) (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
|
||||
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
@@ -18,21 +18,18 @@
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]).
|
||||
- [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] Untested files have been added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
|
||||
|
||||
14
.github/main.workflow
vendored
14
.github/main.workflow
vendored
@@ -1,14 +0,0 @@
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" {
|
||||
on = "issues"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" {
|
||||
on = "pull_request"
|
||||
resolves = "codeowners-mention"
|
||||
}
|
||||
|
||||
action "codeowners-mention" {
|
||||
uses = "home-assistant/codeowners-mention@master"
|
||||
secrets = ["GITHUB_TOKEN"]
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
config/*
|
||||
config2/*
|
||||
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
@@ -84,7 +85,7 @@ Scripts/
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
tags
|
||||
ctags.tmp
|
||||
|
||||
# vagrant stuff
|
||||
@@ -94,6 +95,7 @@ virtualization/vagrant/config
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.devcontainer
|
||||
|
||||
# Built docs
|
||||
docs/build
|
||||
|
||||
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
sudo: false
|
||||
dist: xenial
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "ppa:jonathonf/ffmpeg-4"
|
||||
packages:
|
||||
- libudev-dev
|
||||
- libavformat-dev
|
||||
- libavcodec-dev
|
||||
- libavdevice-dev
|
||||
- libavutil-dev
|
||||
- libswscale-dev
|
||||
- libswresample-dev
|
||||
- libavfilter-dev
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
|
||||
cache: pip
|
||||
install: pip install -U tox
|
||||
language: python
|
||||
script: travis_wait 40 tox --develop
|
||||
67
CODEOWNERS
67
CODEOWNERS
@@ -17,32 +17,39 @@ 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
|
||||
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/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
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
|
||||
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
|
||||
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
|
||||
@@ -56,6 +63,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
|
||||
@@ -64,12 +72,17 @@ 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
|
||||
homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
@@ -78,11 +91,13 @@ homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
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
|
||||
@@ -99,13 +114,16 @@ 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
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/incomfort/* @zxdavb
|
||||
homeassistant/components/influxdb/* @fabaff
|
||||
homeassistant/components/input_boolean/* @home-assistant/core
|
||||
homeassistant/components/input_datetime/* @home-assistant/core
|
||||
@@ -115,6 +133,7 @@ homeassistant/components/input_text/* @home-assistant/core
|
||||
homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/ipma/* @dgomes
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
@@ -122,21 +141,27 @@ 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
|
||||
homeassistant/components/min_max/* @fabaff
|
||||
@@ -150,32 +175,39 @@ homeassistant/components/nello/* @pschmitt
|
||||
homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @awarecan
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
homeassistant/components/notify/* @flowolf
|
||||
homeassistant/components/notify/* @home-assistant/core
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
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/pollen/* @bachya
|
||||
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
|
||||
@@ -183,6 +215,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
|
||||
@@ -191,20 +224,27 @@ 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/sun/* @Swamp-Ig
|
||||
homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switcher_kis/* @tomerfi
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/sytadin/* @gautric
|
||||
@@ -226,7 +266,6 @@ homeassistant/components/tradfri/* @ggravlingen
|
||||
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
|
||||
@@ -235,7 +274,10 @@ homeassistant/components/uptimerobot/* @ludeeus
|
||||
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
|
||||
@@ -244,14 +286,15 @@ homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xmpp/* @fabaff
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
150
azure-pipelines-ci.yml
Normal file
150
azure-pipelines-ci.yml
Normal file
@@ -0,0 +1,150 @@
|
||||
# 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'
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Lint'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
python -m venv lint
|
||||
|
||||
. lint/bin/activate
|
||||
pip install flake8
|
||||
flake8 homeassistant tests script
|
||||
displayName: 'Run flake8'
|
||||
|
||||
|
||||
- job: 'Check'
|
||||
dependsOn:
|
||||
- Lint
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 1
|
||||
matrix:
|
||||
Python35:
|
||||
python.version: '3.5'
|
||||
python.container: '35'
|
||||
Python36:
|
||||
python.version: '3.6'
|
||||
python.container: '36'
|
||||
Python37:
|
||||
python.version: '3.7'
|
||||
python.container: '37'
|
||||
container: $[ variables['python.container'] ]
|
||||
steps:
|
||||
- script: |
|
||||
echo "$(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
|
||||
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)'
|
||||
|
||||
- job: 'FullCheck'
|
||||
dependsOn:
|
||||
- Check
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
container: $[ variables['PythonMain'] ]
|
||||
steps:
|
||||
- script: |
|
||||
echo "$(PythonMain)" > .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'
|
||||
|
||||
168
azure-pipelines-release.yml
Normal file
168
azure-pipelines-release.yml
Normal file
@@ -0,0 +1,168 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionBuilder
|
||||
value: '4.5'
|
||||
- group: docker
|
||||
- group: github
|
||||
- group: twine
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
- job: 'VersionValidate'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
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'
|
||||
|
||||
|
||||
- job: 'ReleasePython'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
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'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
|
||||
dependsOn:
|
||||
- 'VersionValidate'
|
||||
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,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: 'ReleaseHassio'
|
||||
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker'))
|
||||
dependsOn:
|
||||
- 'ReleaseDocker'
|
||||
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'
|
||||
100
azure-pipelines-wheels.yml
Normal file
100
azure-pipelines-wheels.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- dev
|
||||
paths:
|
||||
include:
|
||||
- requirements_all.txt
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: '0.7'
|
||||
- group: wheels
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
- job: 'Wheels'
|
||||
condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master'))
|
||||
timeoutInMinutes: 360
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
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'
|
||||
@@ -7,8 +7,9 @@ import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
||||
|
||||
from typing import ( # noqa pylint: disable=unused-import
|
||||
List, Dict, Any, TYPE_CHECKING
|
||||
)
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
@@ -18,6 +19,9 @@ from homeassistant.const import (
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant import core
|
||||
|
||||
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
@@ -86,10 +90,12 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_file(config_dir: str) -> str:
|
||||
async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
|
||||
-> str:
|
||||
"""Ensure configuration file exists."""
|
||||
import homeassistant.config as config_util
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
config_path = await config_util.async_ensure_config_exists(
|
||||
hass, config_dir)
|
||||
|
||||
if config_path is None:
|
||||
print('Error getting configuration path')
|
||||
@@ -261,6 +267,7 @@ def cmdline() -> List[str]:
|
||||
async def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
from homeassistant import bootstrap, core
|
||||
|
||||
hass = core.HomeAssistant()
|
||||
@@ -275,7 +282,7 @@ async def setup_and_run_hass(config_dir: str,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
config_file = await ensure_config_file(hass, config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
await bootstrap.async_from_config_file(
|
||||
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
@@ -390,7 +397,7 @@ def main() -> int:
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
return exit_code # type: ignore # mypy cannot yet infer it
|
||||
return exit_code # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6']
|
||||
REQUIREMENTS = ['pyotp==2.2.7']
|
||||
|
||||
CONF_MESSAGE = 'message'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1']
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
@@ -11,6 +11,7 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa
|
||||
from .util import test_all
|
||||
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
@@ -29,6 +30,10 @@ class AbstractPermissions:
|
||||
"""Return a function that can test entity access."""
|
||||
raise NotImplementedError
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
raise NotImplementedError
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Check if we can access entity."""
|
||||
entity_func = self._cached_entity_func
|
||||
@@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions):
|
||||
self._policy = policy
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return test_all(self._policy.get(CAT_ENTITIES), key)
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES),
|
||||
@@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions):
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def access_all_entities(self, key: str) -> bool:
|
||||
"""Check if we have a certain access to all entities."""
|
||||
return True
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return lambda entity_id, key: True
|
||||
|
||||
@@ -3,6 +3,7 @@ from functools import wraps
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
@@ -96,3 +97,16 @@ def _gen_dict_test_func(
|
||||
return schema.get(key)
|
||||
|
||||
return test_value
|
||||
|
||||
|
||||
def test_all(policy: CategoryType, key: str) -> bool:
|
||||
"""Test if a policy has an ALL access for a specific key."""
|
||||
if not isinstance(policy, dict):
|
||||
return bool(policy)
|
||||
|
||||
all_policy = policy.get(SUBCAT_ALL)
|
||||
|
||||
if not isinstance(all_policy, dict):
|
||||
return bool(all_policy)
|
||||
|
||||
return all_policy.get(key, False)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -26,6 +25,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {'ptvsd', }
|
||||
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
|
||||
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
@@ -93,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
|
||||
@@ -306,6 +268,15 @@ async def _async_set_up_integrations(
|
||||
"""Set up all the integrations."""
|
||||
domains = _get_domains(hass, config)
|
||||
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
debuggers = domains & DEBUGGER_INTEGRATIONS
|
||||
if debuggers:
|
||||
_LOGGER.debug("Starting up debuggers %s", debuggers)
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in debuggers])
|
||||
domains -= DEBUGGER_INTEGRATIONS
|
||||
|
||||
# Resolve all dependencies of all components so we can find the logging
|
||||
# and integrations that need faster initialization.
|
||||
resolved_domains_task = asyncio.gather(*[
|
||||
@@ -339,7 +310,7 @@ async def _async_set_up_integrations(
|
||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
||||
|
||||
if logging_domains:
|
||||
_LOGGER.debug("Setting up %s", logging_domains)
|
||||
_LOGGER.info("Setting up %s", logging_domains)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
@@ -355,7 +326,7 @@ async def _async_set_up_integrations(
|
||||
if stage_1_domains:
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
for domain in logging_domains
|
||||
for domain in stage_1_domains
|
||||
])
|
||||
|
||||
# Load all integrations
|
||||
|
||||
29
homeassistant/components/adguard/.translations/ca.json
Normal file
29
homeassistant/components/adguard/.translations/ca.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
28
homeassistant/components/adguard/.translations/de.json
Normal file
28
homeassistant/components/adguard/.translations/de.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/en.json
Normal file
29
homeassistant/components/adguard/.translations/en.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/ko.json
Normal file
29
homeassistant/components/adguard/.translations/ko.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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\ud569\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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/lb.json
Normal file
29
homeassistant/components/adguard/.translations/lb.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/nl.json
Normal file
29
homeassistant/components/adguard/.translations/nl.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/no.json
Normal file
29
homeassistant/components/adguard/.translations/no.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/pl.json
Normal file
29
homeassistant/components/adguard/.translations/pl.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/pt-BR.json
Normal file
29
homeassistant/components/adguard/.translations/pt-BR.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/ru.json
Normal file
29
homeassistant/components/adguard/.translations/ru.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/sl.json
Normal file
29
homeassistant/components/adguard/.translations/sl.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/adguard/.translations/zh-Hant.json
Normal file
29
homeassistant/components/adguard/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"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()
|
||||
@@ -31,9 +31,11 @@ CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_VAR_POSITION = 'adsvar_position'
|
||||
|
||||
STATE_KEY_STATE = 'state'
|
||||
STATE_KEY_BRIGHTNESS = 'brightness'
|
||||
STATE_KEY_POSITION = 'position'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
|
||||
165
homeassistant/components/ads/cover.py
Normal file
165
homeassistant/components/ads/cover.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Support for ADS covers."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
|
||||
SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
CoverDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, \
|
||||
AdsEntity, STATE_KEY_STATE, STATE_KEY_POSITION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'ADS Cover'
|
||||
|
||||
CONF_ADS_VAR_SET_POS = 'adsvar_set_position'
|
||||
CONF_ADS_VAR_OPEN = 'adsvar_open'
|
||||
CONF_ADS_VAR_CLOSE = 'adsvar_close'
|
||||
CONF_ADS_VAR_STOP = 'adsvar_stop'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the cover platform for ADS."""
|
||||
ads_hub = hass.data[DATA_ADS]
|
||||
|
||||
ads_var_is_closed = config.get(CONF_ADS_VAR)
|
||||
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
|
||||
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
|
||||
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
|
||||
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
|
||||
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
|
||||
name = config[CONF_NAME]
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
add_entities([AdsCover(ads_hub,
|
||||
ads_var_is_closed,
|
||||
ads_var_position,
|
||||
ads_var_pos_set,
|
||||
ads_var_open,
|
||||
ads_var_close,
|
||||
ads_var_stop,
|
||||
name,
|
||||
device_class)])
|
||||
|
||||
|
||||
class AdsCover(AdsEntity, CoverDevice):
|
||||
"""Representation of ADS cover."""
|
||||
|
||||
def __init__(self, ads_hub,
|
||||
ads_var_is_closed, ads_var_position,
|
||||
ads_var_pos_set, ads_var_open,
|
||||
ads_var_close, ads_var_stop, name, device_class):
|
||||
"""Initialize AdsCover entity."""
|
||||
super().__init__(ads_hub, name, ads_var_is_closed)
|
||||
if self._ads_var is None:
|
||||
if ads_var_position is not None:
|
||||
self._unique_id = ads_var_position
|
||||
elif ads_var_pos_set is not None:
|
||||
self._unique_id = ads_var_pos_set
|
||||
elif ads_var_open is not None:
|
||||
self._unique_id = ads_var_open
|
||||
|
||||
self._state_dict[STATE_KEY_POSITION] = None
|
||||
self._ads_var_position = ads_var_position
|
||||
self._ads_var_pos_set = ads_var_pos_set
|
||||
self._ads_var_open = ads_var_open
|
||||
self._ads_var_close = ads_var_close
|
||||
self._ads_var_stop = ads_var_stop
|
||||
self._device_class = device_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
if self._ads_var is not None:
|
||||
await self.async_initialize_device(self._ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
if self._ads_var_position is not None:
|
||||
await self.async_initialize_device(self._ads_var_position,
|
||||
self._ads_hub.PLCTYPE_BYTE,
|
||||
STATE_KEY_POSITION)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this cover."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._ads_var is not None:
|
||||
return self._state_dict[STATE_KEY_STATE]
|
||||
if self._ads_var_position is not None:
|
||||
return self._state_dict[STATE_KEY_POSITION] == 0
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover."""
|
||||
return self._state_dict[STATE_KEY_POSITION]
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
if self._ads_var_stop is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._ads_var_pos_set is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
return supported_features
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._ads_var_stop:
|
||||
self._ads_hub.write_by_name(self._ads_var_stop, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
if self._ads_var_pos_set is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_pos_set, position,
|
||||
self._ads_hub.PLCTYPE_BYTE)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
if self._ads_var_open is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_open, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
elif self._ads_var_pos_set is not None:
|
||||
self.set_cover_position(position=100)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._ads_var_close is not None:
|
||||
self._ads_hub.write_by_name(self._ads_var_close, True,
|
||||
self._ads_hub.PLCTYPE_BOOL)
|
||||
elif self._ads_var_pos_set is not None:
|
||||
self.set_cover_position(position=0)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return False if state has not been updated yet."""
|
||||
if self._ads_var is not None or self._ads_var_position is not None:
|
||||
return self._state_dict[STATE_KEY_STATE] is not None or \
|
||||
self._state_dict[STATE_KEY_POSITION] is not None
|
||||
return True
|
||||
@@ -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
|
||||
|
||||
0
homeassistant/components/alarmdecoder/services.yaml
Normal file
0
homeassistant/components/alarmdecoder/services.yaml
Normal file
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
597
homeassistant/components/alexa/capabilities.py
Normal file
597
homeassistant/components/alexa/capabilities.py
Normal file
@@ -0,0 +1,597 @@
|
||||
"""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,
|
||||
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.state == STATE_OFF:
|
||||
return 'OFF'
|
||||
return 'ON'
|
||||
|
||||
|
||||
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 = []
|
||||
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_LOW:
|
||||
properties.append({'name': 'lowerSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||
properties.append({'name': 'upperSetpoint'})
|
||||
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||
properties.append({'name': 'thermostatMode'})
|
||||
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':
|
||||
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||
if mode is None:
|
||||
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||
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,15 @@
|
||||
"""Constants for the Alexa integration."""
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import fan
|
||||
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
|
||||
# Flash briefing constants
|
||||
@@ -25,4 +36,73 @@ 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.STATE_HEAT, 'HEAT'),
|
||||
(climate.STATE_COOL, 'COOL'),
|
||||
(climate.STATE_AUTO, 'AUTO'),
|
||||
(climate.STATE_ECO, 'ECO'),
|
||||
(climate.STATE_MANUAL, 'AUTO'),
|
||||
(STATE_OFF, 'OFF'),
|
||||
(climate.STATE_IDLE, 'OFF'),
|
||||
(climate.STATE_FAN_ONLY, 'OFF'),
|
||||
(climate.STATE_DRY, 'OFF'),
|
||||
])
|
||||
|
||||
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'
|
||||
|
||||
459
homeassistant/components/alexa/entities.py
Normal file
459
homeassistant/components/alexa/entities.py
Normal file
@@ -0,0 +1,459 @@
|
||||
"""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."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_ON_OFF:
|
||||
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)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & media_player.const.SUPPORT_VOLUME_SET:
|
||||
yield AlexaSpeaker(self.entity)
|
||||
|
||||
power_features = (media_player.SUPPORT_TURN_ON |
|
||||
media_player.SUPPORT_TURN_OFF)
|
||||
if supported & power_features:
|
||||
yield AlexaPowerController(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'
|
||||
719
homeassistant/components/alexa/handlers.py
Normal file
719
homeassistant/components/alexa/handlers.py
Normal file
@@ -0,0 +1,719 @@
|
||||
"""Alexa message handlers."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant import core as ha
|
||||
from homeassistant.util.decorator import Registry
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
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,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import cover, fan, group, light, media_player
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
Cause,
|
||||
)
|
||||
from .entities import async_get_entities
|
||||
from .state_report import async_enable_proactive_mode
|
||||
from .errors import (
|
||||
AlexaInvalidValueError,
|
||||
AlexaTempRangeError,
|
||||
AlexaUnsupportedThermostatModeError,
|
||||
)
|
||||
|
||||
_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
|
||||
|
||||
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
|
||||
|
||||
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']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
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)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||
}
|
||||
|
||||
response = directive.response()
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_OPERATION_MODE, 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
|
||||
0
homeassistant/components/alexa/services.yaml
Normal file
0
homeassistant/components/alexa/services.yaml
Normal file
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, {})
|
||||
|
||||
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)
|
||||
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.",
|
||||
"already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.",
|
||||
"no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Autenticaci\u00f3 exitosa amb Ambi Climate."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia",
|
||||
"no_token": "No autenticat amb Ambi Climate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i <b>Permet</b> l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem <b>Envia</b> (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
|
||||
"title": "Autenticaci\u00f3 amb Ambi Climate"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal file
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"follow_link": "N\u00e1sledujte odkaz a prove\u010fte ov\u011b\u0159en\u00ed p\u0159ed stisknut\u00edm tla\u010d\u00edtka Odeslat.",
|
||||
"no_token": "Nen\u00ed ov\u011b\u0159en s Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a <b> Povolit </b> p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte <b> Odeslat </b> n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )",
|
||||
"title": "Ov\u011b\u0159it Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/de.json
Normal file
23
homeassistant/components/ambiclimate/.translations/de.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.",
|
||||
"already_setup": "Das Ambiclimate Konto ist konfiguriert.",
|
||||
"no_config": "Ambiclimate muss konfiguriert sein, bevor die Authentifizierund durchgef\u00fchrt werden kann. [Bitte lies die Anleitung] (https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",
|
||||
"no_token": "Nicht authentifiziert mit Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Bitte folge diesem [link] ({authorization_url}) und <b> Erlaube </b> Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke <b> Senden </b> darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)",
|
||||
"title": "Ambiclimate authentifizieren"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/en.json
Normal file
23
homeassistant/components/ambiclimate/.translations/en.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Unknown error generating an access token.",
|
||||
"already_setup": "The Ambiclimate account is configured.",
|
||||
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Please follow the link and authenticate before pressing Submit",
|
||||
"no_token": "Not authenticated with Ambiclimate"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})",
|
||||
"title": "Authenticate Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
@@ -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/ko.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ko.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
|
||||
"no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 <b>\ud5c8\uc6a9</b> \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 <b>Submit</b> \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
|
||||
"title": "Ambi Climate \uc778\uc99d"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal file
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.",
|
||||
"already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.",
|
||||
"no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Follegt w.e.g. dem Link an authentifiz\u00e9iert de Kont ier dir op ofsch\u00e9cken dr\u00e9ckt.",
|
||||
"no_token": "Net mat Ambiclimate authentifiz\u00e9iert"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "Follegt d\u00ebsem [Link]({authorization_url}) an <b>erlaabtt</b> den Acc\u00e8s zu \u00e4rem Ambiclimate Kont , a kommt dann zer\u00e9ck heihin an dr\u00e9ck op <b>ofsch\u00e9cken</b> hei \u00ebnnen.\n(Stellt s\u00e9cher dass den Type vun Callback {cb_url} ass.)",
|
||||
"title": "Ambiclimate authentifiz\u00e9ieren"
|
||||
}
|
||||
},
|
||||
"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/ru.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ru.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
|
||||
"already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
|
||||
"no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
|
||||
"no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430."
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 <b>\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435</b> \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 <b>\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c</b>. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
},
|
||||
"title": "Ambi Climate"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002",
|
||||
"already_setup": "Ambiclimate \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
|
||||
"no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002",
|
||||
"no_token": "Ambiclimate \u672a\u6388\u6b0a"
|
||||
},
|
||||
"step": {
|
||||
"auth": {
|
||||
"description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078<b>\u5141\u8a31</b>\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684<b>\u50b3\u9001</b>\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09",
|
||||
"title": "\u8a8d\u8b49 Ambiclimate"
|
||||
}
|
||||
},
|
||||
"title": "Ambiclimate"
|
||||
}
|
||||
}
|
||||
44
homeassistant/components/ambiclimate/__init__.py
Normal file
44
homeassistant/components/ambiclimate/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Support for Ambiclimate devices."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from . import config_flow
|
||||
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN:
|
||||
vol.Schema({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
})
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up Ambiclimate components."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
config_flow.register_flow_implementation(
|
||||
hass, conf[CONF_CLIENT_ID],
|
||||
conf[CONF_CLIENT_SECRET])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Ambiclimate from a config entry."""
|
||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||
entry, 'climate'))
|
||||
|
||||
return True
|
||||
231
homeassistant/components/ambiclimate/climate.py
Normal file
231
homeassistant/components/ambiclimate/climate.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Support for Ambiclimate ac."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import ambiclimate
|
||||
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)
|
||||
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,
|
||||
DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
|
||||
SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_ON_OFF)
|
||||
|
||||
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
})
|
||||
|
||||
SET_COMFORT_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
})
|
||||
|
||||
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Ambicliamte device."""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the Ambicliamte device from config entry."""
|
||||
config = entry.data
|
||||
websession = async_get_clientsession(hass)
|
||||
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
token_info = await store.async_load()
|
||||
|
||||
oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
|
||||
config[CONF_CLIENT_SECRET],
|
||||
config['callback_url'],
|
||||
websession)
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
await store.async_save(token_info)
|
||||
|
||||
data_connection = ambiclimate.AmbiclimateConnection(oauth,
|
||||
token_info=token_info,
|
||||
websession=websession)
|
||||
|
||||
if not await data_connection.find_devices():
|
||||
_LOGGER.error("No devices found")
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for heater in data_connection.get_devices():
|
||||
tasks.append(heater.update_device_info())
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
devs = []
|
||||
for heater in data_connection.get_devices():
|
||||
devs.append(AmbiclimateEntity(heater, store))
|
||||
|
||||
async_add_entities(devs, True)
|
||||
|
||||
async def send_comfort_feedback(service):
|
||||
"""Send comfort feedback."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_comfort_feedback(service.data[ATTR_VALUE])
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_COMFORT_FEEDBACK,
|
||||
send_comfort_feedback,
|
||||
schema=SEND_COMFORT_FEEDBACK_SCHEMA)
|
||||
|
||||
async def set_comfort_mode(service):
|
||||
"""Set comfort mode."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_comfort_mode()
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_COMFORT_MODE,
|
||||
set_comfort_mode,
|
||||
schema=SET_COMFORT_MODE_SCHEMA)
|
||||
|
||||
async def set_temperature_mode(service):
|
||||
"""Set temperature mode."""
|
||||
device_name = service.data[ATTR_NAME]
|
||||
device = data_connection.find_device_by_room_name(device_name)
|
||||
if device:
|
||||
await device.set_temperature_mode(service.data[ATTR_VALUE])
|
||||
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_TEMPERATURE_MODE,
|
||||
set_temperature_mode,
|
||||
schema=SET_TEMPERATURE_MODE_SCHEMA)
|
||||
|
||||
|
||||
class AmbiclimateEntity(ClimateDevice):
|
||||
"""Representation of a Ambiclimate Thermostat device."""
|
||||
|
||||
def __init__(self, heater, store):
|
||||
"""Initialize the thermostat."""
|
||||
self._heater = heater
|
||||
self._store = store
|
||||
self._data = {}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._heater.device_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._heater.name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, self.unique_id)
|
||||
},
|
||||
'name': self.name,
|
||||
'manufacturer': 'Ambiclimate',
|
||||
}
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
return self._data.get('target_temperature')
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._data.get('temperature')
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""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."""
|
||||
return self._heater.get_min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._heater.get_max_temp()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
return STATE_HEAT if self.is_on else STATE_OFF
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
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_update(self):
|
||||
"""Retrieve latest state."""
|
||||
try:
|
||||
token_info = await self._heater.control.refresh_access_token()
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to refresh access token")
|
||||
return
|
||||
|
||||
if token_info:
|
||||
await self._store.async_save(token_info)
|
||||
|
||||
self._data = await self._heater.update_device()
|
||||
153
homeassistant/components/ambiclimate/config_flow.py
Normal file
153
homeassistant/components/ambiclimate/config_flow.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Config flow for Ambiclimate."""
|
||||
import logging
|
||||
|
||||
import ambiclimate
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def register_flow_implementation(hass, client_id, client_secret):
|
||||
"""Register a ambiclimate implementation.
|
||||
|
||||
client_id: Client id.
|
||||
client_secret: Client secret.
|
||||
"""
|
||||
hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {})
|
||||
|
||||
hass.data[DATA_AMBICLIMATE_IMPL] = {
|
||||
CONF_CLIENT_ID: client_id,
|
||||
CONF_CLIENT_SECRET: client_secret,
|
||||
}
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register('ambiclimate')
|
||||
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize flow."""
|
||||
self._registered_view = False
|
||||
self._oauth = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle external yaml configuration."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
|
||||
|
||||
if not config:
|
||||
_LOGGER.debug("No config")
|
||||
return self.async_abort(reason='no_config')
|
||||
|
||||
return await self.async_step_auth()
|
||||
|
||||
async def async_step_auth(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
errors['base'] = 'follow_link'
|
||||
|
||||
if not self._registered_view:
|
||||
self._generate_view()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='auth',
|
||||
description_placeholders={'authorization_url':
|
||||
await self._get_authorize_url(),
|
||||
'cb_url': self._cb_url()},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_code(self, code=None):
|
||||
"""Received code for authentication."""
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason='already_setup')
|
||||
|
||||
token_info = await self._get_token_info(code)
|
||||
|
||||
if token_info is None:
|
||||
return self.async_abort(reason='access_token')
|
||||
|
||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
|
||||
config['callback_url'] = self._cb_url()
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Ambiclimate",
|
||||
data=config,
|
||||
)
|
||||
|
||||
async def _get_token_info(self, code):
|
||||
oauth = self._generate_oauth()
|
||||
try:
|
||||
token_info = await oauth.get_access_token(code)
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to get access token", exc_info=True)
|
||||
return None
|
||||
|
||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
await store.async_save(token_info)
|
||||
|
||||
return token_info
|
||||
|
||||
def _generate_view(self):
|
||||
self.hass.http.register_view(AmbiclimateAuthCallbackView())
|
||||
self._registered_view = True
|
||||
|
||||
def _generate_oauth(self):
|
||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
|
||||
clientsession = async_get_clientsession(self.hass)
|
||||
callback_url = self._cb_url()
|
||||
|
||||
oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
|
||||
config.get(CONF_CLIENT_SECRET),
|
||||
callback_url,
|
||||
clientsession)
|
||||
return oauth
|
||||
|
||||
def _cb_url(self):
|
||||
return '{}{}'.format(self.hass.config.api.base_url,
|
||||
AUTH_CALLBACK_PATH)
|
||||
|
||||
async def _get_authorize_url(self):
|
||||
oauth = self._generate_oauth()
|
||||
return oauth.get_authorize_url()
|
||||
|
||||
|
||||
class AmbiclimateAuthCallbackView(HomeAssistantView):
|
||||
"""Ambiclimate Authorization Callback View."""
|
||||
|
||||
requires_auth = False
|
||||
url = AUTH_CALLBACK_PATH
|
||||
name = AUTH_CALLBACK_NAME
|
||||
|
||||
async def get(self, request):
|
||||
"""Receive authorization token."""
|
||||
code = request.query.get('code')
|
||||
if code is None:
|
||||
return "No code"
|
||||
hass = request.app['hass']
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={'source': 'code'},
|
||||
data=code,
|
||||
))
|
||||
return "OK!"
|
||||
14
homeassistant/components/ambiclimate/const.py
Normal file
14
homeassistant/components/ambiclimate/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants used by the Ambiclimate component."""
|
||||
|
||||
ATTR_VALUE = 'value'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
DOMAIN = 'ambiclimate'
|
||||
SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
|
||||
SERVICE_COMFORT_MODE = 'set_comfort_mode'
|
||||
SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
|
||||
STORAGE_KEY = 'ambiclimate_auth'
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
AUTH_CALLBACK_NAME = 'api:ambiclimate'
|
||||
AUTH_CALLBACK_PATH = '/api/ambiclimate'
|
||||
13
homeassistant/components/ambiclimate/manifest.json
Normal file
13
homeassistant/components/ambiclimate/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "ambiclimate",
|
||||
"name": "Ambiclimate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ambiclimate",
|
||||
"requirements": [
|
||||
"ambiclimate==0.2.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@danielhiversen"
|
||||
]
|
||||
}
|
||||
36
homeassistant/components/ambiclimate/services.yaml
Normal file
36
homeassistant/components/ambiclimate/services.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# Describes the format for available services for ambiclimate
|
||||
|
||||
set_comfort_mode:
|
||||
description: >
|
||||
Enable comfort mode on your AC
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
|
||||
send_comfort_feedback:
|
||||
description: >
|
||||
Send feedback for comfort mode
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
Value:
|
||||
description: >
|
||||
Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
|
||||
example: bit_warm
|
||||
|
||||
set_temperature_mode:
|
||||
description: >
|
||||
Enable temperature mode on your AC
|
||||
fields:
|
||||
Name:
|
||||
description: >
|
||||
String with device name.
|
||||
example: Bedroom
|
||||
Value:
|
||||
description: >
|
||||
Target value in celsius
|
||||
example: 22
|
||||
23
homeassistant/components/ambiclimate/strings.json
Normal file
23
homeassistant/components/ambiclimate/strings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Ambiclimate",
|
||||
"step": {
|
||||
"auth": {
|
||||
"title": "Authenticate Ambiclimate",
|
||||
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})"
|
||||
}
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Ambiclimate"
|
||||
},
|
||||
"error": {
|
||||
"no_token": "Not authenticated with Ambiclimate",
|
||||
"follow_link": "Please follow the link and authenticate before pressing Submit"
|
||||
},
|
||||
"abort": {
|
||||
"already_setup": "The Ambiclimate account is configured.",
|
||||
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
|
||||
"access_token": "Unknown error generating an access token."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -1,33 +1,49 @@
|
||||
"""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
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION)
|
||||
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
|
||||
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
|
||||
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, dispatcher_send)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
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_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
MAX_ERRORS = 5
|
||||
RECHECK_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
@@ -43,104 +59,143 @@ AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_sensors(value):
|
||||
if SENSOR_MOTION_DETECTOR in value:
|
||||
def _deprecated_sensor_values(sensors):
|
||||
if SENSOR_MOTION_DETECTOR in sensors:
|
||||
_LOGGER.warning(
|
||||
'sensors option %s is deprecated. '
|
||||
'Please remove from your configuration and '
|
||||
'use binary_sensors option motion_detected instead.',
|
||||
SENSOR_MOTION_DETECTOR)
|
||||
return value
|
||||
"The '%s' option value '%s' is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"the '%s' option with value '%s' instead",
|
||||
CONF_SENSORS, SENSOR_MOTION_DETECTOR, CONF_BINARY_SENSORS,
|
||||
BINARY_SENSOR_MOTION_DETECTED)
|
||||
return sensors
|
||||
|
||||
|
||||
def _has_unique_names(value):
|
||||
names = [camera[CONF_NAME] for camera in value]
|
||||
def _deprecated_switches(config):
|
||||
if CONF_SWITCHES in config:
|
||||
_LOGGER.warning(
|
||||
"The '%s' option (with value %s) is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"services and attributes instead",
|
||||
CONF_SWITCHES, config[CONF_SWITCHES])
|
||||
return config
|
||||
|
||||
|
||||
def _has_unique_names(devices):
|
||||
names = [device[CONF_NAME] for device in devices]
|
||||
vol.Schema(vol.Unique())(names)
|
||||
return value
|
||||
return devices
|
||||
|
||||
|
||||
AMCREST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})
|
||||
AMCREST_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)],
|
||||
_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
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
|
||||
}, 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, {})
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
for device in config[DOMAIN]:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
camera = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.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]
|
||||
@@ -148,7 +203,8 @@ def setup(hass, config):
|
||||
binary_sensors = device.get(CONF_BINARY_SENSORS)
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
|
||||
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,48 +213,99 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
hass.data[DATA_AMCREST][DEVICES][name] = AmcrestDevice(
|
||||
api, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution, control_light)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
hass, CAMERA, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
|
||||
if binary_sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
hass, BINARY_SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_BINARY_SENSORS: binary_sensors
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
hass, SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
hass, SWITCH, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return len(hass.data[DATA_AMCREST]) >= 1
|
||||
if not hass.data[DATA_AMCREST][DEVICES]:
|
||||
return False
|
||||
|
||||
def have_permission(user, entity_id):
|
||||
return not user or user.permissions.check_entity(
|
||||
entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call):
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
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]
|
||||
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]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context,
|
||||
entity_id=entity_id,
|
||||
permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call):
|
||||
args = []
|
||||
for arg in CAMERA_SERVICES[call.service][2]:
|
||||
args.append(call.data[arg])
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
service_signal(call.service, entity_id),
|
||||
*args
|
||||
)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler, params[0])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
def __init__(self, api, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution, control_light):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
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,43 +2,61 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from amcrest import AmcrestError
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_MOTION)
|
||||
BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION)
|
||||
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
|
||||
from . import DATA_AMCREST, 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=5)
|
||||
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 = {
|
||||
BINARY_SENSOR_MOTION_DETECTED: ('Motion Detected', DEVICE_CLASS_MOTION),
|
||||
BINARY_SENSOR_ONLINE: ('Online', DEVICE_CLASS_CONNECTIVITY),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a binary sensor for an Amcrest IP Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
binary_sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_binary_sensors = []
|
||||
for sensor_type in binary_sensors:
|
||||
amcrest_binary_sensors.append(
|
||||
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_devices(amcrest_binary_sensors, True)
|
||||
name = discovery_info[CONF_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]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestBinarySensor(BinarySensorDevice):
|
||||
"""Binary sensor for Amcrest camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._camera = camera
|
||||
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):
|
||||
@@ -53,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._camera.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,19 +1,79 @@
|
||||
"""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 (
|
||||
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT
|
||||
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',
|
||||
'rtsp',
|
||||
]
|
||||
|
||||
_SRV_EN_REC = 'enable_recording'
|
||||
_SRV_DS_REC = 'disable_recording'
|
||||
_SRV_EN_AUD = 'enable_audio'
|
||||
_SRV_DS_AUD = 'disable_audio'
|
||||
_SRV_EN_MOT_REC = 'enable_motion_recording'
|
||||
_SRV_DS_MOT_REC = 'disable_motion_recording'
|
||||
_SRV_GOTO = 'goto_preset'
|
||||
_SRV_CBW = 'set_color_bw'
|
||||
_SRV_TOUR_ON = 'start_tour'
|
||||
_SRV_TOUR_OFF = 'stop_tour'
|
||||
|
||||
_ATTR_PRESET = 'preset'
|
||||
_ATTR_COLOR_BW = 'color_bw'
|
||||
|
||||
_CBW_COLOR = 'color'
|
||||
_CBW_AUTO = 'auto'
|
||||
_CBW_BW = 'bw'
|
||||
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
|
||||
|
||||
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
|
||||
})
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
|
||||
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
|
||||
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
|
||||
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
|
||||
_SRV_EN_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
|
||||
_SRV_DS_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
|
||||
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
|
||||
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
|
||||
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
|
||||
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
|
||||
}
|
||||
|
||||
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
@@ -21,61 +81,76 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities([
|
||||
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, amcrest):
|
||||
def __init__(self, name, device, ffmpeg):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._api = device.api
|
||||
self._ffmpeg = ffmpeg
|
||||
self._ffmpeg_arguments = device.ffmpeg_arguments
|
||||
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._camera.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):
|
||||
"""Return an MJPEG stream."""
|
||||
# The snapshot implementation is handled by the parent class
|
||||
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
|
||||
if self._stream_source == 'snapshot':
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
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)
|
||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
||||
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
streaming_url, auth=self._token,
|
||||
timeout=CAMERA_WEB_SESSION_TIMEOUT)
|
||||
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
@@ -83,7 +158,7 @@ class AmcrestCam(Camera):
|
||||
# streaming via ffmpeg
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.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)
|
||||
@@ -98,11 +173,37 @@ 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."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the Amcrest-specific camera state attributes."""
|
||||
attr = {}
|
||||
if self._audio_enabled is not None:
|
||||
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
|
||||
if self._motion_recording_enabled is not None:
|
||||
attr['motion_recording'] = _BOOL_TO_STATE.get(
|
||||
self._motion_recording_enabled)
|
||||
if self._color_bw is not None:
|
||||
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."""
|
||||
@@ -118,17 +219,21 @@ class AmcrestCam(Camera):
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
return self._brand
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_detection_enabled
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""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._camera.rtsp_url(typeno=self._resolution)
|
||||
return self._rtsp_url
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -137,26 +242,63 @@ 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():
|
||||
self._unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass,
|
||||
service_signal(service, self.entity_id),
|
||||
getattr(self, params[1])))
|
||||
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)
|
||||
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._camera.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:
|
||||
self.is_streaming = self._camera.video_enabled
|
||||
self._is_recording = self._camera.record_mode == 'Manual'
|
||||
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 = (
|
||||
self._api.is_motion_detector_on())
|
||||
self._audio_enabled = self._api.audio_enabled
|
||||
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
|
||||
|
||||
@@ -168,18 +310,174 @@ class AmcrestCam(Camera):
|
||||
"""Turn on camera."""
|
||||
self._enable_video_stream(True)
|
||||
|
||||
# Utility methods
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
self._enable_motion_detection(True)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
self._enable_motion_detection(False)
|
||||
|
||||
# Additional Amcrest Camera service methods
|
||||
|
||||
async def async_enable_recording(self):
|
||||
"""Call the job and enable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, True)
|
||||
|
||||
async def async_disable_recording(self):
|
||||
"""Call the job and disable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, False)
|
||||
|
||||
async def async_enable_audio(self):
|
||||
"""Call the job and enable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, True)
|
||||
|
||||
async def async_disable_audio(self):
|
||||
"""Call the job and disable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, False)
|
||||
|
||||
async def async_enable_motion_recording(self):
|
||||
"""Call the job and enable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
True)
|
||||
|
||||
async def async_disable_motion_recording(self):
|
||||
"""Call the job and disable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
False)
|
||||
|
||||
async def async_goto_preset(self, preset):
|
||||
"""Call the job and move camera to preset position."""
|
||||
await self.hass.async_add_executor_job(self._goto_preset, preset)
|
||||
|
||||
async def async_set_color_bw(self, color_bw):
|
||||
"""Call the job and set camera color mode."""
|
||||
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
|
||||
|
||||
async def async_start_tour(self):
|
||||
"""Call the job and start camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, True)
|
||||
|
||||
async def async_stop_tour(self):
|
||||
"""Call the job and stop camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, False)
|
||||
|
||||
# Methods to send commands to Amcrest camera and handle errors
|
||||
|
||||
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.
|
||||
if self.is_recording and not enable:
|
||||
self._enable_recording(False)
|
||||
try:
|
||||
self._camera.video_enabled = enable
|
||||
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."""
|
||||
# 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.
|
||||
if not self.is_streaming and enable:
|
||||
self._enable_video_stream(True)
|
||||
rec_mode = {'Automatic': 0, 'Manual': 1}
|
||||
try:
|
||||
self._api.record_mode = rec_mode[
|
||||
'Manual' if enable else 'Automatic']
|
||||
except AmcrestError as 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."""
|
||||
try:
|
||||
self._api.motion_detection = str(enable).lower()
|
||||
except AmcrestError as 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."""
|
||||
try:
|
||||
self._api.audio_enabled = enable
|
||||
except AmcrestError as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'enable' if enable else 'disable', self.name,
|
||||
'camera audio stream', error)
|
||||
else:
|
||||
self._audio_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
if self._control_light:
|
||||
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."""
|
||||
try:
|
||||
self._api.motion_recording = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'enable' if enable else 'disable', self.name,
|
||||
'camera motion recording', error)
|
||||
else:
|
||||
self._motion_recording_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _goto_preset(self, preset):
|
||||
"""Move camera position and zoom to preset."""
|
||||
try:
|
||||
self._api.go_to_preset(
|
||||
action='start', preset_point_number=preset)
|
||||
except AmcrestError as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'move', self.name,
|
||||
'camera to preset {}'.format(preset), error)
|
||||
|
||||
def _set_color_bw(self, cbw):
|
||||
"""Set camera color mode."""
|
||||
try:
|
||||
self._api.day_night_color = _CBW.index(cbw)
|
||||
except AmcrestError as 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."""
|
||||
try:
|
||||
self._api.tour(start=start)
|
||||
except AmcrestError as error:
|
||||
log_update_error(
|
||||
_LOGGER, 'start' if start else 'stop', self.name,
|
||||
'camera tour', error)
|
||||
|
||||
11
homeassistant/components/amcrest/const.py
Normal file
11
homeassistant/components/amcrest/const.py
Normal file
@@ -0,0 +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'
|
||||
17
homeassistant/components/amcrest/helpers.py
Normal file
17
homeassistant/components/amcrest/helpers.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Helpers for amcrest component."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def service_signal(service, ident=None):
|
||||
"""Encode service and identifier into signal."""
|
||||
signal = '{}_{}'.format(DOMAIN, service)
|
||||
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.3.0"
|
||||
"amcrest==1.5.3"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg"
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
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 . import DATA_AMCREST, SENSORS
|
||||
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=10)
|
||||
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
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'],
|
||||
SENSOR_PTZ_PRESET: ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
SENSOR_SDCARD: ['SD Used', '%', 'mdi:sd'],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@@ -18,30 +33,28 @@ async def async_setup_platform(
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
sensors = discovery_info[CONF_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_sensors = []
|
||||
for sensor_type in sensors:
|
||||
amcrest_sensors.append(
|
||||
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_entities(amcrest_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities(
|
||||
[AmcrestSensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSensor(Entity):
|
||||
"""A sensor implementation for Amcrest IP camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize a sensor for Amcrest camera."""
|
||||
self._attrs = {}
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._name = '{0}_{1}'.format(
|
||||
name, SENSORS.get(self._sensor_type)[0])
|
||||
self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
|
||||
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):
|
||||
@@ -66,22 +79,55 @@ class AmcrestSensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return SENSORS.get(self._sensor_type)[1]
|
||||
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._camera.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._camera.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._camera.ptz_presets_count
|
||||
elif self._sensor_type == SENSOR_PTZ_PRESET:
|
||||
self._state = self._api.ptz_presets_count
|
||||
|
||||
elif self._sensor_type == 'sdcard':
|
||||
sd_used = self._camera.storage_used
|
||||
sd_total = self._camera.storage_total
|
||||
self._attrs['Total'] = '{0} {1}'.format(*sd_total)
|
||||
self._attrs['Used'] = '{0} {1}'.format(*sd_used)
|
||||
self._state = self._camera.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()
|
||||
|
||||
75
homeassistant/components/amcrest/services.yaml
Normal file
75
homeassistant/components/amcrest/services.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
enable_recording:
|
||||
description: Enable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_recording:
|
||||
description: Disable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_audio:
|
||||
description: Enable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_audio:
|
||||
description: Disable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_motion_recording:
|
||||
description: Enable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_motion_recording:
|
||||
description: Disable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
goto_preset:
|
||||
description: Move camera to PTZ preset.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
preset:
|
||||
description: Preset number, starting from 1.
|
||||
example: 1
|
||||
|
||||
set_color_bw:
|
||||
description: Set camera color mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
color_bw:
|
||||
description: Color mode, one of 'auto', 'color' or 'bw'.
|
||||
example: auto
|
||||
|
||||
start_tour:
|
||||
description: Start camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
stop_tour:
|
||||
description: Stop camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
@@ -1,13 +1,25 @@
|
||||
"""Support for toggling Amcrest IP camera settings."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON
|
||||
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 . import DATA_AMCREST, SWITCHES
|
||||
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']
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
@@ -16,69 +28,95 @@ async def async_setup_platform(
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
switches = discovery_info[CONF_SWITCHES]
|
||||
camera = hass.data[DATA_AMCREST][name].device
|
||||
|
||||
all_switches = []
|
||||
|
||||
for setting in switches:
|
||||
all_switches.append(AmcrestSwitch(setting, camera, name))
|
||||
|
||||
async_add_entities(all_switches, True)
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities(
|
||||
[AmcrestSwitch(name, device, setting)
|
||||
for setting in discovery_info[CONF_SWITCHES]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSwitch(ToggleEntity):
|
||||
"""Representation of an Amcrest IP camera switch."""
|
||||
|
||||
def __init__(self, setting, camera, name):
|
||||
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._camera = camera
|
||||
self._name = '{} {}'.format(SWITCHES[setting][0], name)
|
||||
self._state = False
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._state = None
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the switch."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state == STATE_ON
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn setting on."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'true'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.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._camera.motion_detection = 'false'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.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._camera.is_motion_detector_on()
|
||||
elif self._setting == 'motion_recording':
|
||||
detection = self._camera.is_record_on_motion_detection()
|
||||
|
||||
self._state = STATE_ON if detection else STATE_OFF
|
||||
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.16"
|
||||
],
|
||||
"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):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user