forked from home-assistant/core
Compare commits
932 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ec37834b | ||
|
|
3d841681d7 | ||
|
|
295a004326 | ||
|
|
8de0824688 | ||
|
|
edc1cbdc32 | ||
|
|
1788eaf037 | ||
|
|
4750656f1a | ||
|
|
011cc624b6 | ||
|
|
2d9a964953 | ||
|
|
87133a0e77 | ||
|
|
fe8dec27a3 | ||
|
|
b5323cd894 | ||
|
|
23316a8344 | ||
|
|
86ecd7555a | ||
|
|
02f55b039c | ||
|
|
ebaf7f8c00 | ||
|
|
355005114b | ||
|
|
8f529b20d7 | ||
|
|
b2faa67ab7 | ||
|
|
95371fe4a6 | ||
|
|
2d980f2a92 | ||
|
|
b6d3a199ce | ||
|
|
731753b604 | ||
|
|
cf24687024 | ||
|
|
ef93d48d50 | ||
|
|
9982867d66 | ||
|
|
bdfd473aaa | ||
|
|
7e3d0f0700 | ||
|
|
c03b137130 | ||
|
|
85dbf1eed3 | ||
|
|
237ac08076 | ||
|
|
2e973c7572 | ||
|
|
e980d1b9fe | ||
|
|
26b7c2de7e | ||
|
|
ab826b8fe2 | ||
|
|
a9a8cbbd10 | ||
|
|
3655fefec2 | ||
|
|
ff33cbd22f | ||
|
|
e343f5521c | ||
|
|
f7bc44955c | ||
|
|
df2c3cdded | ||
|
|
f504e5ef61 | ||
|
|
8bf58e1df5 | ||
|
|
90183bd682 | ||
|
|
88ec73ed8f | ||
|
|
7baffed7b7 | ||
|
|
cc4d29d42a | ||
|
|
10c1378195 | ||
|
|
cf3a97ff3c | ||
|
|
222ba96b3e | ||
|
|
6239a523cc | ||
|
|
7eb6e49df7 | ||
|
|
7e91c0dc83 | ||
|
|
b1a05e7605 | ||
|
|
cd90bb4161 | ||
|
|
17d0fe02c7 | ||
|
|
43b140be5e | ||
|
|
9ded16ccc3 | ||
|
|
91dc0c3731 | ||
|
|
6e4a99cec0 | ||
|
|
20bd14defb | ||
|
|
20fa6c5383 | ||
|
|
e2a1e21c8d | ||
|
|
45878c6df0 | ||
|
|
3e5233d115 | ||
|
|
b50c93ccb7 | ||
|
|
50a66abd80 | ||
|
|
7106d9e9d4 | ||
|
|
daf9d28565 | ||
|
|
33860bf23c | ||
|
|
fa196e5889 | ||
|
|
9500bb1ac8 | ||
|
|
b3548f1ead | ||
|
|
f74e976be1 | ||
|
|
dc55718bc3 | ||
|
|
6551c53e32 | ||
|
|
a27d49d022 | ||
|
|
95c43d634b | ||
|
|
a5b9f5040f | ||
|
|
e83a9aace4 | ||
|
|
1e4463957d | ||
|
|
326787ef1a | ||
|
|
15f4ed74ac | ||
|
|
b7b4224429 | ||
|
|
5088e7ee49 | ||
|
|
11004bcf34 | ||
|
|
83db673bd0 | ||
|
|
6b3e4ca7bd | ||
|
|
aa176312a5 | ||
|
|
6235aae196 | ||
|
|
764ea06795 | ||
|
|
9c52a3ce22 | ||
|
|
c6d9ceca63 | ||
|
|
fee87cd6ed | ||
|
|
71ab8a9b1a | ||
|
|
a795093705 | ||
|
|
0c0c471447 | ||
|
|
dc7e5e3af4 | ||
|
|
a9389d2d43 | ||
|
|
993a02b8c4 | ||
|
|
29c2b2fe79 | ||
|
|
73197c9a6c | ||
|
|
1a5048baaf | ||
|
|
9718a17351 | ||
|
|
6feacbbfe1 | ||
|
|
4ea71b0602 | ||
|
|
2ceb4d2d1e | ||
|
|
708334c0c2 | ||
|
|
b5272f2bc7 | ||
|
|
80867cc9b7 | ||
|
|
f92b392a24 | ||
|
|
220054a6c3 | ||
|
|
0904ff45fe | ||
|
|
d6752d2270 | ||
|
|
373e3b12d8 | ||
|
|
e985f30247 | ||
|
|
22bf4d0783 | ||
|
|
1cbb5b8e51 | ||
|
|
ac79ff9e24 | ||
|
|
0bf10b0b09 | ||
|
|
31981fde7e | ||
|
|
4fce051838 | ||
|
|
bd450ee9ff | ||
|
|
879924fea4 | ||
|
|
3b0db291dd | ||
|
|
a71cc67efb | ||
|
|
c5905ee5ca | ||
|
|
3edcc9420a | ||
|
|
b022dde622 | ||
|
|
1187e0aea5 | ||
|
|
a401be9b1b | ||
|
|
49a9b61f6f | ||
|
|
ed683d8c2c | ||
|
|
d4061b73b0 | ||
|
|
9305ea9a6b | ||
|
|
7b0c88c54b | ||
|
|
253e154a79 | ||
|
|
daf48a3b1f | ||
|
|
5ac0469ef9 | ||
|
|
fccaf7f919 | ||
|
|
f198859441 | ||
|
|
a302112879 | ||
|
|
6a8eb8d0a1 | ||
|
|
f23708ce6f | ||
|
|
0dd3640c78 | ||
|
|
d0da265166 | ||
|
|
3ca3fe7015 | ||
|
|
ef8253c549 | ||
|
|
78e29cd3fa | ||
|
|
9c178cf488 | ||
|
|
e6d002c377 | ||
|
|
70281a148b | ||
|
|
d4b092706a | ||
|
|
db536797be | ||
|
|
d9d27733d1 | ||
|
|
5f16f3c3a6 | ||
|
|
d3672f36fb | ||
|
|
3cf6c76f8b | ||
|
|
434d1d7d63 | ||
|
|
401e22fc0c | ||
|
|
8eb4e77365 | ||
|
|
1f863830e1 | ||
|
|
cd29b47924 | ||
|
|
edb3722abd | ||
|
|
bc036cc2fe | ||
|
|
8778b707f1 | ||
|
|
05ae8f9dd4 | ||
|
|
d199f57aa8 | ||
|
|
f47e080f37 | ||
|
|
cfc5ebbfb0 | ||
|
|
6971e84ddf | ||
|
|
397c4336bc | ||
|
|
e00ed84d84 | ||
|
|
7b28963a88 | ||
|
|
5a00cc5afc | ||
|
|
cb3d62eeef | ||
|
|
241d87e9d3 | ||
|
|
b2789d9883 | ||
|
|
7bb60068d7 | ||
|
|
1c23a36f46 | ||
|
|
0ea5a73e8d | ||
|
|
6df3c480b3 | ||
|
|
61f7a39748 | ||
|
|
5961f2f577 | ||
|
|
61bf4d8a29 | ||
|
|
44477f3d32 | ||
|
|
ed45dff6e8 | ||
|
|
2a35a3901e | ||
|
|
ebff253cc9 | ||
|
|
f5d3aa1826 | ||
|
|
cffb704311 | ||
|
|
58af332d21 | ||
|
|
ef2c8b2e5b | ||
|
|
9fa7906aef | ||
|
|
c6c5d40056 | ||
|
|
d7cd1a2b4b | ||
|
|
7e8973a315 | ||
|
|
3f87d41381 | ||
|
|
8d9da4e7b9 | ||
|
|
93e3596e5a | ||
|
|
c434ad6af5 | ||
|
|
99c6622ee2 | ||
|
|
9d4dbd7d97 | ||
|
|
fa3f6ca2c7 | ||
|
|
1d78393680 | ||
|
|
951d7154b8 | ||
|
|
3f28b30860 | ||
|
|
83dec7173c | ||
|
|
d16e6c8524 | ||
|
|
052c094425 | ||
|
|
40e0966d7f | ||
|
|
ad4d5666fe | ||
|
|
7f896bfb40 | ||
|
|
83dd961fde | ||
|
|
a1dac28e4b | ||
|
|
e5c3a4be80 | ||
|
|
707b7c202d | ||
|
|
78c38749ab | ||
|
|
670c75e844 | ||
|
|
419725e1a9 | ||
|
|
cfc175d71d | ||
|
|
8310f4a1cf | ||
|
|
5022cf8a6c | ||
|
|
2cd99e5a97 | ||
|
|
26f2e3dd8b | ||
|
|
fc67f5eef3 | ||
|
|
2aeb0efc7c | ||
|
|
a99ba0a1d4 | ||
|
|
e903f7ffda | ||
|
|
cf249e3e5e | ||
|
|
a8a21ee28d | ||
|
|
1a76603f53 | ||
|
|
089e15e046 | ||
|
|
b59d69f313 | ||
|
|
eb7db1f763 | ||
|
|
429f09deb3 | ||
|
|
5167658a1d | ||
|
|
6bf3f9e748 | ||
|
|
9d56730b8d | ||
|
|
26cf5acd5b | ||
|
|
9190fe1c21 | ||
|
|
0c34c50d2f | ||
|
|
757ba3b60e | ||
|
|
882c4b73ae | ||
|
|
4455a287fc | ||
|
|
5db7d702c8 | ||
|
|
540d22d603 | ||
|
|
56a43436d7 | ||
|
|
1393766659 | ||
|
|
68d72931c4 | ||
|
|
0c0184973b | ||
|
|
59ec469722 | ||
|
|
68472b8241 | ||
|
|
9380fca97e | ||
|
|
c3b1121d77 | ||
|
|
4d3d51635d | ||
|
|
a372053eac | ||
|
|
2e120061b4 | ||
|
|
3a6eac216c | ||
|
|
6671cbb96d | ||
|
|
9e386938bb | ||
|
|
6be52208fc | ||
|
|
dd55ff87c8 | ||
|
|
b637b48bd8 | ||
|
|
42fb886d71 | ||
|
|
9290f245bf | ||
|
|
c41ef65da6 | ||
|
|
744dd42ad3 | ||
|
|
27f0331f0f | ||
|
|
394ffc40b6 | ||
|
|
d5f5273c31 | ||
|
|
af2402ea59 | ||
|
|
71a0274b12 | ||
|
|
3f498bd042 | ||
|
|
ee5e1fa355 | ||
|
|
f2d8f3bcb8 | ||
|
|
ff4204244b | ||
|
|
a54e242245 | ||
|
|
849665b9ca | ||
|
|
315f83e1ea | ||
|
|
4b7f85518f | ||
|
|
59d78b060f | ||
|
|
bd4ff6fc21 | ||
|
|
5fcea074c4 | ||
|
|
7369af0639 | ||
|
|
b872cb95c7 | ||
|
|
3338ebd4c1 | ||
|
|
7b176aa7c9 | ||
|
|
c722091967 | ||
|
|
e922dd10ba | ||
|
|
4cffef7f2b | ||
|
|
31b1b0c044 | ||
|
|
2de1193fd9 | ||
|
|
c12bbddc0b | ||
|
|
086c71525e | ||
|
|
06a64c0167 | ||
|
|
c1ed9edd26 | ||
|
|
1d7d82fde5 | ||
|
|
592e1dc96a | ||
|
|
6e81ae096e | ||
|
|
c8266c6692 | ||
|
|
8fda705377 | ||
|
|
5d6562a73f | ||
|
|
9285831fa1 | ||
|
|
760047f964 | ||
|
|
8683eeb908 | ||
|
|
75e236de73 | ||
|
|
169abe637c | ||
|
|
07d90c6c55 | ||
|
|
a66db59359 | ||
|
|
bed1b96f5a | ||
|
|
13106a9b55 | ||
|
|
7598067b55 | ||
|
|
7a22fbe961 | ||
|
|
738089c57c | ||
|
|
5e7d4a57a3 | ||
|
|
1061c369f1 | ||
|
|
88c0a92857 | ||
|
|
0c770520ed | ||
|
|
92e28067d8 | ||
|
|
d1636dab31 | ||
|
|
13aa935147 | ||
|
|
29c3f1618f | ||
|
|
37a47b5a59 | ||
|
|
2e62afabdc | ||
|
|
a8f5e8699a | ||
|
|
4218efddcd | ||
|
|
aec320dc19 | ||
|
|
acf684de05 | ||
|
|
ce1e8f8ace | ||
|
|
b55c7a5ba2 | ||
|
|
52ff232797 | ||
|
|
769dda735d | ||
|
|
02bf07d9df | ||
|
|
b92b24e8a4 | ||
|
|
0cfbb9ce91 | ||
|
|
c9976718d4 | ||
|
|
b61a250321 | ||
|
|
ae0dd33e2e | ||
|
|
0d93bf9a45 | ||
|
|
178bf736f6 | ||
|
|
a559c06d6b | ||
|
|
cc1891ef2b | ||
|
|
05d8c57212 | ||
|
|
3abdf217bb | ||
|
|
6a0c9a718e | ||
|
|
abd329d707 | ||
|
|
04fdde0e86 | ||
|
|
2b3019bdf4 | ||
|
|
cf0147098a | ||
|
|
aeb21596a0 | ||
|
|
cf5f02b347 | ||
|
|
467a59a6ed | ||
|
|
da622abb79 | ||
|
|
16cbc2d07f | ||
|
|
952a1b3513 | ||
|
|
a5402739b7 | ||
|
|
704c9d8582 | ||
|
|
2e5eb4d9dc | ||
|
|
5d693277f0 | ||
|
|
3cb20c7b4d | ||
|
|
4210835dcd | ||
|
|
15a160a630 | ||
|
|
c78850a983 | ||
|
|
8e3a70e568 | ||
|
|
06340c9875 | ||
|
|
31dd327c59 | ||
|
|
38e2926a48 | ||
|
|
19722a0ef8 | ||
|
|
a6f8c3f662 | ||
|
|
11d5671ee0 | ||
|
|
20601cfc2d | ||
|
|
07972c84a1 | ||
|
|
7f0a50ce31 | ||
|
|
03845ef490 | ||
|
|
37706c2731 | ||
|
|
71e3047f5c | ||
|
|
0954eefa9f | ||
|
|
90f71261c5 | ||
|
|
13af61e103 | ||
|
|
b0b3620b2b | ||
|
|
e07b6dfe0b | ||
|
|
3f5018459f | ||
|
|
2feab82396 | ||
|
|
1decba0052 | ||
|
|
1667481342 | ||
|
|
8e276295eb | ||
|
|
284d4d49c7 | ||
|
|
c3eff5773b | ||
|
|
4d471622f6 | ||
|
|
90f2990b9e | ||
|
|
b197b8bab3 | ||
|
|
a148f3e2a9 | ||
|
|
d732f8eca2 | ||
|
|
b0c1c37cd5 | ||
|
|
4c36ffd0ef | ||
|
|
fbc1c41673 | ||
|
|
2e6346ca43 | ||
|
|
122e21a3c4 | ||
|
|
33a54043e9 | ||
|
|
d09ee8ac82 | ||
|
|
1ca2e5226b | ||
|
|
b5e3d8c337 | ||
|
|
f0fbdd6a26 | ||
|
|
e5c0e4336d | ||
|
|
c69a790ede | ||
|
|
5bc2e78ab4 | ||
|
|
68bda1c732 | ||
|
|
3137099348 | ||
|
|
22a80cf733 | ||
|
|
9edf1e5151 | ||
|
|
6159f8b0ce | ||
|
|
d4cde2bfbf | ||
|
|
760f822dce | ||
|
|
b24f9f5dfa | ||
|
|
134eeecd65 | ||
|
|
3b5e5cbcd6 | ||
|
|
9e4c8f45d6 | ||
|
|
121dba659c | ||
|
|
9aaf11de8c | ||
|
|
ea7b1e4573 | ||
|
|
8444b9ba03 | ||
|
|
38e371c5d9 | ||
|
|
750c96709e | ||
|
|
940d5fb2ee | ||
|
|
0a2b266742 | ||
|
|
f511920a04 | ||
|
|
1b7bfec247 | ||
|
|
f5632a5da5 | ||
|
|
1e28c752e1 | ||
|
|
677714ecab | ||
|
|
e2aadc3227 | ||
|
|
c45b240026 | ||
|
|
32d652884b | ||
|
|
3528f8e647 | ||
|
|
3e6cf8f59f | ||
|
|
06d959ed43 | ||
|
|
7f47d601f1 | ||
|
|
8b1bdda0fa | ||
|
|
70ce9bb7bc | ||
|
|
45fdda3f5d | ||
|
|
caaf4f5694 | ||
|
|
3ddad83a84 | ||
|
|
35b6064581 | ||
|
|
c600d28b6a | ||
|
|
909c3bdd63 | ||
|
|
4b8c38819e | ||
|
|
7d1960baba | ||
|
|
af89e7c50f | ||
|
|
c93879074b | ||
|
|
f476d781ec | ||
|
|
720b05c301 | ||
|
|
c9b6567265 | ||
|
|
9abdbf3db6 | ||
|
|
f879ac0993 | ||
|
|
70b901017f | ||
|
|
7bfe0e1c00 | ||
|
|
fb39641eef | ||
|
|
b7e03f6973 | ||
|
|
7597e30efb | ||
|
|
a7248d4574 | ||
|
|
51dbc988f9 | ||
|
|
71333a15f7 | ||
|
|
bac71d3d22 | ||
|
|
d77e88645e | ||
|
|
2d104f95d7 | ||
|
|
81e21b90c9 | ||
|
|
d1ad2cc225 | ||
|
|
8d65230a36 | ||
|
|
2cc6263092 | ||
|
|
ad79dc673d | ||
|
|
da3342f1aa | ||
|
|
29db43edb2 | ||
|
|
24e9c62fe7 | ||
|
|
81d4338b93 | ||
|
|
96e5acda1a | ||
|
|
726cf9b1c4 | ||
|
|
631ecf578e | ||
|
|
273a7af330 | ||
|
|
4b674b1d16 | ||
|
|
dd45e99302 | ||
|
|
d0ddc28f96 | ||
|
|
9ab8f78b19 | ||
|
|
732009c668 | ||
|
|
d20e0f5c95 | ||
|
|
98a4b1e9ac | ||
|
|
917df1af00 | ||
|
|
f13f723a04 | ||
|
|
7b68f344e3 | ||
|
|
824e59499f | ||
|
|
ff9377d1d9 | ||
|
|
cf0d0fb33e | ||
|
|
75c372021d | ||
|
|
3aaf619fc3 | ||
|
|
e375b63902 | ||
|
|
e5861241c7 | ||
|
|
e205092693 | ||
|
|
fa98a27df7 | ||
|
|
c899875abb | ||
|
|
bab079f649 | ||
|
|
92a5068977 | ||
|
|
3cba2e695c | ||
|
|
bfa1c55803 | ||
|
|
ba2b8512c5 | ||
|
|
e2a56721d3 | ||
|
|
672fc61bb2 | ||
|
|
6490ec87f3 | ||
|
|
dc102a96a9 | ||
|
|
b91a061cef | ||
|
|
7eaf8640d0 | ||
|
|
8e311686d0 | ||
|
|
5a22e7d211 | ||
|
|
7de0e1e39a | ||
|
|
4501bdb4a0 | ||
|
|
4a265f37e0 | ||
|
|
c3f58b8c74 | ||
|
|
422ccc1a28 | ||
|
|
eb59f2dd3c | ||
|
|
399040de46 | ||
|
|
0dbfd77402 | ||
|
|
61a2d09342 | ||
|
|
345c886dec | ||
|
|
b9043ef7a7 | ||
|
|
356040d506 | ||
|
|
0431e38aa2 | ||
|
|
4c32ad3b48 | ||
|
|
bc8d323bdd | ||
|
|
a1c914dfeb | ||
|
|
093285f92f | ||
|
|
9ea5afd109 | ||
|
|
01925fdfff | ||
|
|
7840b1e387 | ||
|
|
e4898bb05c | ||
|
|
f4974f58fe | ||
|
|
2b2502c91c | ||
|
|
4a4c07ac1b | ||
|
|
03bce84c32 | ||
|
|
4f9fc9b39f | ||
|
|
069b819679 | ||
|
|
90197b6ec9 | ||
|
|
42790d3e97 | ||
|
|
e78f4d1b65 | ||
|
|
354c8f3409 | ||
|
|
579b77ba4c | ||
|
|
b52e8525ac | ||
|
|
dc75db6376 | ||
|
|
7d68ec1110 | ||
|
|
c352b6fa59 | ||
|
|
0bd94d8b56 | ||
|
|
3e2a9afff0 | ||
|
|
d4b239d1d4 | ||
|
|
b2a9e203f2 | ||
|
|
dc1534c6d1 | ||
|
|
589554ad16 | ||
|
|
33d6c99f19 | ||
|
|
1f74adae2a | ||
|
|
7a77951bb4 | ||
|
|
ad47ece5c6 | ||
|
|
5ee4718e24 | ||
|
|
a5cb4e6c2b | ||
|
|
4fd2f773ad | ||
|
|
564ad7e22a | ||
|
|
329d9dfc06 | ||
|
|
2258c56d34 | ||
|
|
e90abf1007 | ||
|
|
eaee55175b | ||
|
|
539b86e079 | ||
|
|
127395ae8d | ||
|
|
eca1f050cd | ||
|
|
0d681b0ba6 | ||
|
|
94d38a1c6b | ||
|
|
cfd1aec741 | ||
|
|
eec6722cf4 | ||
|
|
fce275d29c | ||
|
|
5eda5f2f7b | ||
|
|
5ab580ab34 | ||
|
|
5613816476 | ||
|
|
e9c7fe184d | ||
|
|
41bb4760f7 | ||
|
|
ee3f17d5c7 | ||
|
|
18d37ff0fd | ||
|
|
7fe0d8b2f4 | ||
|
|
8b42d0c471 | ||
|
|
213171769d | ||
|
|
d5813cf167 | ||
|
|
3e59ffb33a | ||
|
|
a0a54dfd5b | ||
|
|
3bfe9e757e | ||
|
|
9fdf123a2f | ||
|
|
aeaf694552 | ||
|
|
3d1c8ee467 | ||
|
|
98b92c78c0 | ||
|
|
2ac16b12c1 | ||
|
|
df67093441 | ||
|
|
c475a876ce | ||
|
|
90c18d1c15 | ||
|
|
78b6439ee6 | ||
|
|
092c146eae | ||
|
|
44a98fb77a | ||
|
|
03d93bea34 | ||
|
|
9dbac9b033 | ||
|
|
9e86f0498b | ||
|
|
03de658d4d | ||
|
|
3ea8c25e1f | ||
|
|
6e01ea5929 | ||
|
|
c6ccbed828 | ||
|
|
e58836f99f | ||
|
|
874225dd67 | ||
|
|
0d0d5c8c2c | ||
|
|
fc6cc22b6d | ||
|
|
39ea9a8c90 | ||
|
|
c7d5f7698e | ||
|
|
9bbd61cbe4 | ||
|
|
7a7a164cb8 | ||
|
|
8379567636 | ||
|
|
93af3c57ff | ||
|
|
d1acb0326c | ||
|
|
35005474f8 | ||
|
|
3a45481b5b | ||
|
|
aa7635398a | ||
|
|
d3658c4af9 | ||
|
|
dfe38b4d5a | ||
|
|
fcb84d951e | ||
|
|
27eede724c | ||
|
|
258beb9cd3 | ||
|
|
da882672bd | ||
|
|
60dfd68083 | ||
|
|
6e4a6cc069 | ||
|
|
a1c524d372 | ||
|
|
3160fa5de8 | ||
|
|
d4b7057a3d | ||
|
|
227a1b919b | ||
|
|
0121e3cb04 | ||
|
|
da108f1999 | ||
|
|
d376049a3f | ||
|
|
7f462ba0ec | ||
|
|
b7ef4dddb4 | ||
|
|
b72499a949 | ||
|
|
b770acd9a7 | ||
|
|
56b0d2e99f | ||
|
|
8e7f783da8 | ||
|
|
1913d07c39 | ||
|
|
2a85ed7236 | ||
|
|
cba3a5b055 | ||
|
|
d2246d5a4f | ||
|
|
72419a1afe | ||
|
|
a7325ebe1f | ||
|
|
27d50d388f | ||
|
|
25712f16b3 | ||
|
|
5cb0c92e78 | ||
|
|
a7a16e4317 | ||
|
|
c19665fed4 | ||
|
|
6588dceadd | ||
|
|
9e59fc5d05 | ||
|
|
366e270e94 | ||
|
|
4b30cbbf3b | ||
|
|
41ac2a3c73 | ||
|
|
b8257866f5 | ||
|
|
849a93e0a6 | ||
|
|
f918d62571 | ||
|
|
1c251009fe | ||
|
|
201fd4afee | ||
|
|
3e0c6c176a | ||
|
|
44fdfdf695 | ||
|
|
fea1c921fc | ||
|
|
5e3e441aa0 | ||
|
|
a1e6e04a5e | ||
|
|
2002497d09 | ||
|
|
baeb791d84 | ||
|
|
9c9df793b4 | ||
|
|
05922ac56a | ||
|
|
34deaf8849 | ||
|
|
cc38981a38 | ||
|
|
18ce5092b4 | ||
|
|
7f7372198a | ||
|
|
abe61c5529 | ||
|
|
b231fa2616 | ||
|
|
336289d7e7 | ||
|
|
19514ea500 | ||
|
|
00918af94d | ||
|
|
e054e4da1b | ||
|
|
0c945d81c8 | ||
|
|
1ca09ea36f | ||
|
|
8ce2d701c2 | ||
|
|
9c1a539f90 | ||
|
|
ca7911ec47 | ||
|
|
0d0bda9658 | ||
|
|
7705666061 | ||
|
|
e82e75baf3 | ||
|
|
481f6e09fa | ||
|
|
72e746d240 | ||
|
|
05c0717167 | ||
|
|
67b5b5bc85 | ||
|
|
d076251b18 | ||
|
|
e59ba28fe6 | ||
|
|
f63dba5521 | ||
|
|
f43d9ba680 | ||
|
|
aec134c47a | ||
|
|
901dd9acca | ||
|
|
7a52bbdf24 | ||
|
|
f2203e52ef | ||
|
|
1586d3000c | ||
|
|
d0aeb90c22 | ||
|
|
cb542a90eb | ||
|
|
3824582e68 | ||
|
|
2682d26f79 | ||
|
|
308b7fb385 | ||
|
|
a88cda44d9 | ||
|
|
08100a485a | ||
|
|
4c1b816bb8 | ||
|
|
1983361373 | ||
|
|
4efe86327d | ||
|
|
ff78a5b04b | ||
|
|
68c21530ca | ||
|
|
453cbb7c60 | ||
|
|
77026a2242 | ||
|
|
501f2b0a93 | ||
|
|
117ea9e553 | ||
|
|
beed82ab12 | ||
|
|
601f2df5a7 | ||
|
|
34d369ba26 | ||
|
|
e2465da7c2 | ||
|
|
6bd120ff1d | ||
|
|
188f5de5ae | ||
|
|
6d33fb2dc8 | ||
|
|
e50fc69c1e | ||
|
|
20f06f4eb9 | ||
|
|
37c8aac76f | ||
|
|
20e562b816 | ||
|
|
a4cec0b871 | ||
|
|
60b45d4ba5 | ||
|
|
1ea8c1ece3 | ||
|
|
44d210698e | ||
|
|
968809c991 | ||
|
|
06af76404f | ||
|
|
b9b8bc678c | ||
|
|
629c4a0bf5 | ||
|
|
00d8907c5e | ||
|
|
3f06b4eb9b | ||
|
|
0db13a99aa | ||
|
|
4e3faf6108 | ||
|
|
9583947012 | ||
|
|
50fb59477a | ||
|
|
20f6cb7cc7 | ||
|
|
6b08e6e769 | ||
|
|
ee696643cd | ||
|
|
a059cc860a | ||
|
|
cfe5db4350 | ||
|
|
dcd7b9a529 | ||
|
|
f858938ada | ||
|
|
e96635b5c1 | ||
|
|
d2d715faa8 | ||
|
|
7a5e828f6b | ||
|
|
99f7b7f42d | ||
|
|
3f97944f6b | ||
|
|
1d6609e386 | ||
|
|
fb15dbf3ba | ||
|
|
c20f147949 | ||
|
|
8ed4d732ac | ||
|
|
a1578e3c6e | ||
|
|
253e787a1b | ||
|
|
0d7ee9b93b | ||
|
|
d6d4ff6888 | ||
|
|
4291bdc6b2 | ||
|
|
7d590a6b93 | ||
|
|
e3e3ed42ec | ||
|
|
e7b8d2e6df | ||
|
|
9944c60311 | ||
|
|
93143384a8 | ||
|
|
8a2bc99f63 | ||
|
|
6433f2e2e3 | ||
|
|
50266e9b91 | ||
|
|
7ad094b0a7 | ||
|
|
a5715c48a4 | ||
|
|
d0f9d125a7 | ||
|
|
80c77b8696 | ||
|
|
1f73840aab | ||
|
|
fbaa489533 | ||
|
|
cff9b1bf7e | ||
|
|
ce06229c42 | ||
|
|
3acb3a86cf | ||
|
|
db1bda2975 | ||
|
|
2640db1522 | ||
|
|
cf4b72e00e | ||
|
|
5bd9be6252 | ||
|
|
e1084e3953 | ||
|
|
3afc983c05 | ||
|
|
746f4ac158 | ||
|
|
e1501c83f8 | ||
|
|
3bd12fcef6 | ||
|
|
85658b6dd1 | ||
|
|
e61ac1a4a1 | ||
|
|
8fa9992589 | ||
|
|
f96aee2832 | ||
|
|
7a6facc875 | ||
|
|
7ea482cb1d | ||
|
|
a4aa30fc73 | ||
|
|
ba63a6abc0 | ||
|
|
2252f4a257 | ||
|
|
00cba29ae1 | ||
|
|
f4a54e2483 | ||
|
|
cd473643fe | ||
|
|
4063b24ddb | ||
|
|
46734b8409 | ||
|
|
b9c80a6bb3 | ||
|
|
a2a447b466 | ||
|
|
669b3458b9 | ||
|
|
bf29cbd381 | ||
|
|
1966597d5e | ||
|
|
4685a2cd97 | ||
|
|
78fcea25bb | ||
|
|
ac3700d1c4 | ||
|
|
03480dc779 | ||
|
|
85c40d29ee | ||
|
|
a5cff9877e | ||
|
|
b29c296ced | ||
|
|
a4a175440b | ||
|
|
357e5eadb8 | ||
|
|
52e922171d | ||
|
|
97695a30f5 | ||
|
|
1d12c7b0e7 | ||
|
|
15ad82b9bd | ||
|
|
03fb2b32a6 | ||
|
|
87eb6cd25a | ||
|
|
3797b6b012 | ||
|
|
2b0b431a2a | ||
|
|
e75a1690d1 | ||
|
|
444df5b09a | ||
|
|
b31890c4cb | ||
|
|
8bec4a55d1 | ||
|
|
a5d95dfbdc | ||
|
|
50f63ed4c5 | ||
|
|
901cfef78e | ||
|
|
2c7d6ee6b5 | ||
|
|
c9e34e236d | ||
|
|
38318ed15f | ||
|
|
553d3c0179 | ||
|
|
d3791fa45d | ||
|
|
fa81385b5c | ||
|
|
7d852a985c | ||
|
|
976626d0ab | ||
|
|
d705375a9a | ||
|
|
5e8a1496d7 | ||
|
|
bc618af193 | ||
|
|
0e076fb9e7 | ||
|
|
16a58bd1cf | ||
|
|
8be7a0a9b9 | ||
|
|
232076b41d | ||
|
|
efa9c82c38 | ||
|
|
93f45779c6 | ||
|
|
26d39d39ea | ||
|
|
b43c47cb17 | ||
|
|
67d8db2c9f | ||
|
|
3cbf8e4f87 | ||
|
|
20879726b0 | ||
|
|
f20a3313b0 | ||
|
|
54c3f4f001 | ||
|
|
7289d5b656 | ||
|
|
645c3a67d8 | ||
|
|
88f72a654a | ||
|
|
87df102772 | ||
|
|
25ee8e551c | ||
|
|
3db766e2ec | ||
|
|
1eac6408f5 | ||
|
|
99d48795b9 | ||
|
|
5681fa8f07 | ||
|
|
867d17b03d | ||
|
|
7751dd7535 | ||
|
|
16a885824d | ||
|
|
3934f7bf3a | ||
|
|
96cf6d59a3 | ||
|
|
d46a1a266d | ||
|
|
aaa1ebeed5 | ||
|
|
18ba50bc2d | ||
|
|
3df8840fee | ||
|
|
630b5df59a | ||
|
|
e8801ee22f | ||
|
|
74c0429437 | ||
|
|
563588651c | ||
|
|
63614a477a | ||
|
|
f891d0f5be | ||
|
|
257b8b9b80 | ||
|
|
9a786e449b | ||
|
|
09dc4d663d | ||
|
|
12709ceaa3 | ||
|
|
67df162bcc | ||
|
|
a14980716d | ||
|
|
376d4e4fa0 | ||
|
|
5397c0d73a | ||
|
|
8ab31fe139 | ||
|
|
45649824ca | ||
|
|
6f0c30ff84 | ||
|
|
943260fcd6 | ||
|
|
24aa580b63 | ||
|
|
8435d2f53d | ||
|
|
c51170ef6d | ||
|
|
9d491f5322 | ||
|
|
94662620e2 | ||
|
|
f1e378bff8 | ||
|
|
2e9db1f5c4 | ||
|
|
dec2d8d5b0 | ||
|
|
a439690bd7 | ||
|
|
5d7a2f92df | ||
|
|
4da719f43c | ||
|
|
bacecb4249 | ||
|
|
47755fb1e9 | ||
|
|
69d104bcb6 | ||
|
|
b043ac0f7f | ||
|
|
499bb3f4a2 | ||
|
|
d166f2da80 | ||
|
|
3032de1dc1 | ||
|
|
5341785aae | ||
|
|
289b1802fd | ||
|
|
0da3e73765 | ||
|
|
0a7055d475 | ||
|
|
a1ce14e70f | ||
|
|
2f2bcf0058 | ||
|
|
f929c38e98 | ||
|
|
617802653f | ||
|
|
26a485d43c | ||
|
|
456aa5a2b2 | ||
|
|
97173f495c | ||
|
|
24a8d60566 | ||
|
|
69cea6001f | ||
|
|
647b3ff0fe | ||
|
|
84365cde07 | ||
|
|
e91a1529e4 | ||
|
|
e8775ba2b4 | ||
|
|
e73f31d829 | ||
|
|
839b58c600 | ||
|
|
71d00062e5 | ||
|
|
80f98b9ea2 |
72
.coveragerc
72
.coveragerc
@@ -28,6 +28,9 @@ omit =
|
||||
homeassistant/components/apple_tv.py
|
||||
homeassistant/components/*/apple_tv.py
|
||||
|
||||
homeassistant/components/aqualogic.py
|
||||
homeassistant/components/*/aqualogic.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
@@ -42,6 +45,7 @@ omit =
|
||||
|
||||
homeassistant/components/asterisk_mbox.py
|
||||
homeassistant/components/*/asterisk_mbox.py
|
||||
homeassistant/components/*/asterisk_cdr.py
|
||||
|
||||
homeassistant/components/august.py
|
||||
homeassistant/components/*/august.py
|
||||
@@ -52,7 +56,7 @@ omit =
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
homeassistant/components/blink.py
|
||||
homeassistant/components/blink/*
|
||||
homeassistant/components/*/blink.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
@@ -92,17 +96,26 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/edp_redy.py
|
||||
homeassistant/components/*/edp_redy.py
|
||||
|
||||
homeassistant/components/egardia.py
|
||||
homeassistant/components/*/egardia.py
|
||||
|
||||
homeassistant/components/elkm1/*
|
||||
homeassistant/components/*/elkm1.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/evohome.py
|
||||
homeassistant/components/*/evohome.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/switch/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
|
||||
homeassistant/components/ecovacs.py
|
||||
homeassistant/components/*/ecovacs.py
|
||||
@@ -116,11 +129,15 @@ omit =
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/habitica/*
|
||||
homeassistant/components/*/habitica.py
|
||||
|
||||
homeassistant/components/hangouts/__init__.py
|
||||
homeassistant/components/hangouts/const.py
|
||||
homeassistant/components/hangouts/hangouts_bot.py
|
||||
homeassistant/components/hangouts/hangups_utils.py
|
||||
homeassistant/components/*/hangouts.py
|
||||
homeassistant/components/hangouts/intents.py
|
||||
homeassistant/components/*/hangouts.py
|
||||
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
@@ -137,17 +154,20 @@ omit =
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/huawei_lte.py
|
||||
homeassistant/components/*/huawei_lte.py
|
||||
|
||||
homeassistant/components/hydrawise.py
|
||||
homeassistant/components/*/hydrawise.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/*/insteon.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
@@ -180,6 +200,9 @@ omit =
|
||||
homeassistant/components/linode.py
|
||||
homeassistant/components/*/linode.py
|
||||
|
||||
homeassistant/components/logi_circle.py
|
||||
homeassistant/components/*/logi_circle.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -225,7 +248,10 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/openuv.py
|
||||
homeassistant/components/opentherm_gw.py
|
||||
homeassistant/components/*/opentherm_gw.py
|
||||
|
||||
homeassistant/components/openuv/__init__.py
|
||||
homeassistant/components/*/openuv.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
@@ -264,6 +290,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/*/simplisafe.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
|
||||
@@ -296,6 +325,9 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/tibber/*
|
||||
homeassistant/components/*/tibber.py
|
||||
|
||||
homeassistant/components/toon.py
|
||||
homeassistant/components/*/toon.py
|
||||
|
||||
@@ -356,7 +388,7 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/zoneminder/*
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/tuya.py
|
||||
@@ -372,8 +404,8 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/ifttt.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
||||
homeassistant/components/apiai.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
@@ -402,7 +434,6 @@ omit =
|
||||
homeassistant/components/camera/xeoma.py
|
||||
homeassistant/components/camera/xiaomi.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
@@ -410,6 +441,7 @@ omit =
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/mill.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
@@ -426,7 +458,6 @@ omit =
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/ryobi_gdo.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
@@ -453,6 +484,7 @@ omit =
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/quantum_gateway.py
|
||||
homeassistant/components/device_tracker/ritassist.py
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
@@ -494,6 +526,7 @@ omit =
|
||||
homeassistant/components/light/lw12wifi.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/opple.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
@@ -535,6 +568,7 @@ omit =
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/lg_soundbar.py
|
||||
homeassistant/components/media_player/liveboxplaytv.py
|
||||
homeassistant/components/media_player/mediaroom.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
@@ -578,6 +612,7 @@ omit =
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/hipchat.py
|
||||
homeassistant/components/notify/homematic.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
@@ -604,13 +639,13 @@ omit =
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/notify/yessssms.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/rainbird.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/route53.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
@@ -663,6 +698,7 @@ omit =
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
homeassistant/components/sensor/geizhals.py
|
||||
homeassistant/components/sensor/gitlab_ci.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
@@ -671,6 +707,7 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@@ -679,6 +716,7 @@ omit =
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linky.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
@@ -692,6 +730,7 @@ omit =
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||
homeassistant/components/sensor/netatmo_public.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/netdata_public.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@@ -718,6 +757,7 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/rtorrent.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
@@ -735,6 +775,7 @@ omit =
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/spotcrime.py
|
||||
homeassistant/components/sensor/starlingbank.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
@@ -746,7 +787,7 @@ omit =
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/thermoworks_smoke.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
@@ -754,11 +795,11 @@ omit =
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/uscis.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/volkszaehler.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/waze_travel_time.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
@@ -787,8 +828,11 @@ omit =
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/recswitch.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/switchbot.py
|
||||
homeassistant/components/switch/switchmate.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
@@ -801,10 +845,12 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/water_heater/econet.py
|
||||
homeassistant/components/watson_iot.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
homeassistant/components/weather/met.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,7 +3,7 @@
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR 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>
|
||||
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
@@ -15,7 +15,7 @@
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
- [ ] 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:
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
|
||||
150
CODEOWNERS
150
CODEOWNERS
@@ -2,56 +2,69 @@
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
# Home Assistant Core
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/conversation/* @home-assistant/core
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/lovelace/* @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
homeassistant/components/zone/* @home-assistant/core
|
||||
|
||||
# HomeAssistant developer Teams
|
||||
# Home Assistant Developer Teams
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
homeassistant/components/hassio.py @home-assistant/hassio
|
||||
homeassistant/components/hassio/* @home-assistant/hassio
|
||||
|
||||
# Individual components
|
||||
# Individual platforms
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/binary_sensor/threshold.py @fabaff
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/mill.py @danielhiversen
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/huawei_router.py @abmantis
|
||||
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/influx.py @fabaff
|
||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/lock/nello.py @pschmitt
|
||||
@@ -61,62 +74,173 @@ homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/mpd.py @fabaff
|
||||
homeassistant/components/media_player/sonos.py @amelchio
|
||||
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/no_ip.py @fabaff
|
||||
homeassistant/components/notify/file.py @fabaff
|
||||
homeassistant/components/notify/flock.py @fabaff
|
||||
homeassistant/components/notify/instapush.py @fabaff
|
||||
homeassistant/components/notify/mastodon.py @fabaff
|
||||
homeassistant/components/notify/smtp.py @fabaff
|
||||
homeassistant/components/notify/syslog.py @fabaff
|
||||
homeassistant/components/notify/xmpp.py @fabaff
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/alpha_vantage.py @fabaff
|
||||
homeassistant/components/sensor/bitcoin.py @fabaff
|
||||
homeassistant/components/sensor/cpuspeed.py @fabaff
|
||||
homeassistant/components/sensor/cups.py @fabaff
|
||||
homeassistant/components/sensor/darksky.py @fabaff
|
||||
homeassistant/components/sensor/file.py @fabaff
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/fixer.py @fabaff
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/gitter.py @fabaff
|
||||
homeassistant/components/sensor/glances.py @fabaff
|
||||
homeassistant/components/sensor/gpsd.py @fabaff
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/luftdaten.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
homeassistant/components/sensor/moon.py @fabaff
|
||||
homeassistant/components/sensor/netdata.py @fabaff
|
||||
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
|
||||
homeassistant/components/sensor/pi_hole.py @fabaff
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/pvoutput.py @fabaff
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/shodan.py @fabaff
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/statistics.py @fabaff
|
||||
homeassistant/components/sensor/swiss*.py @fabaff
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/upnp.py @dgomes
|
||||
homeassistant/components/sensor/time_data.py @fabaff
|
||||
homeassistant/components/sensor/version.py @fabaff
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/sensor/worldclock.py @fabaff
|
||||
homeassistant/components/shiftr.py @fabaff
|
||||
homeassistant/components/spaceapi.py @fabaff
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/weather/__init__.py @fabaff
|
||||
homeassistant/components/weather/darksky.py @fabaff
|
||||
homeassistant/components/weather/demo.py @fabaff
|
||||
homeassistant/components/weather/met.py @danielhiversen
|
||||
homeassistant/components/weather/openweathermap.py @fabaff
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
# A
|
||||
homeassistant/components/arduino.py @fabaff
|
||||
homeassistant/components/*/arduino.py @fabaff
|
||||
homeassistant/components/*/arest.py @fabaff
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
|
||||
# B
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/*/blink.py @fronzbot
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
|
||||
# C
|
||||
homeassistant/components/counter/* @fabaff
|
||||
|
||||
# D
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/digital_ocean.py @fabaff
|
||||
homeassistant/components/*/digital_ocean.py @fabaff
|
||||
homeassistant/components/dweet.py @fabaff
|
||||
homeassistant/components/*/dweet.py @fabaff
|
||||
|
||||
# E
|
||||
homeassistant/components/ecovacs.py @OverloadUT
|
||||
homeassistant/components/*/ecovacs.py @OverloadUT
|
||||
homeassistant/components/*/edp_redy.py @abmantis
|
||||
homeassistant/components/edp_redy.py @abmantis
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
|
||||
# H
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/huawei_lte.py @scop
|
||||
homeassistant/components/*/huawei_lte.py @scop
|
||||
|
||||
# K
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
|
||||
# L
|
||||
homeassistant/components/lifx.py @amelchio
|
||||
homeassistant/components/*/lifx.py @amelchio
|
||||
|
||||
# M
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/openuv.py @bachya
|
||||
homeassistant/components/melissa.py @kennedyshead
|
||||
homeassistant/components/*/melissa.py @kennedyshead
|
||||
homeassistant/components/*/mystrom.py @fabaff
|
||||
|
||||
# O
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
|
||||
# Q
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
|
||||
# R
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/*/rainmachine.py @bachya
|
||||
homeassistant/components/*/random.py @fabaff
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
# S
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/*/simplisafe.py @bachya
|
||||
|
||||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/thethingsnetwork.py @fabaff
|
||||
homeassistant/components/*/thethingsnetwork.py @fabaff
|
||||
homeassistant/components/tibber/* @danielhiversen
|
||||
homeassistant/components/*/tibber.py @danielhiversen
|
||||
homeassistant/components/tradfri/* @ggravlingen
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
|
||||
# U
|
||||
homeassistant/components/unifi.py @kane610
|
||||
homeassistant/components/switch/unifi.py @kane610
|
||||
homeassistant/components/upcloud.py @scop
|
||||
homeassistant/components/*/upcloud.py @scop
|
||||
|
||||
# V
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
|
||||
# X
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
# Z
|
||||
homeassistant/components/zoneminder/ @rohankapoorcom
|
||||
homeassistant/components/*/zoneminder.py @rohankapoorcom
|
||||
|
||||
# Other code
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
@@ -10,5 +10,5 @@ The process is straight-forward.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should take a peek at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.
|
||||
|
||||
|
||||
331
LICENSE.md
331
LICENSE.md
@@ -1,194 +1,201 @@
|
||||
Apache License
|
||||
==============
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
1. Definitions.
|
||||
|
||||
#### 1. Definitions
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
#### 4. Redistribution
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
#### 6. Trademarks
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -21,8 +21,8 @@ Featured integrations
|
||||
|
||||
|screenshot-components|
|
||||
|
||||
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
|
||||
components <https://home-assistant.io/developers/creating_components/>`__.
|
||||
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://developers.home-assistant.io/docs/en/architecture_index.html>`__ and the `section on creating your own
|
||||
components <https://developers.home-assistant.io/docs/en/creating_component_index.html>`__.
|
||||
|
||||
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.
|
||||
|
||||
@@ -19,4 +19,4 @@ Indices and tables
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
.. _Home Assistant developers: https://home-assistant.io/developers/
|
||||
.. _Home Assistant developers: https://developers.home-assistant.io/
|
||||
|
||||
@@ -7,7 +7,6 @@ import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
||||
|
||||
|
||||
@@ -20,15 +19,34 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
|
||||
def attempt_use_uvloop() -> None:
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
import asyncio
|
||||
from asyncio.events import BaseDefaultEventLoopPolicy
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
policy = None
|
||||
|
||||
if sys.platform == 'win32':
|
||||
if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'):
|
||||
# pylint: disable=no-member
|
||||
policy = asyncio.WindowsProactorEventLoopPolicy()
|
||||
else:
|
||||
class ProactorPolicy(BaseDefaultEventLoopPolicy):
|
||||
"""Event loop policy to create proactor loops."""
|
||||
|
||||
_loop_factory = asyncio.ProactorEventLoop
|
||||
|
||||
policy = ProactorPolicy()
|
||||
else:
|
||||
try:
|
||||
import uvloop
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
policy = uvloop.EventLoopPolicy()
|
||||
|
||||
if policy is not None:
|
||||
asyncio.set_event_loop_policy(policy)
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
@@ -240,51 +258,39 @@ def cmdline() -> List[str]:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
async def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant import bootstrap, core
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
nt_args = cmdline() + ['--runner']
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(nt_args)
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if exc.returncode != RESTART_EXIT_CODE:
|
||||
sys.exit(exc.returncode)
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
} # type: Dict[str, Any]
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
bootstrap.async_from_config_dict(
|
||||
config, hass, config_dir=config_dir, verbose=args.verbose,
|
||||
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)
|
||||
print('Config directory:', config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
await bootstrap.async_from_config_file(
|
||||
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
||||
log_no_color=args.log_no_color)
|
||||
|
||||
if hass is None:
|
||||
return -1
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
def open_browser(_: Any) -> None:
|
||||
"""Open the web interface in a browser."""
|
||||
if hass.config.api is not None: # type: ignore
|
||||
if hass.config.api is not None:
|
||||
import webbrowser
|
||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
|
||||
run_callback_threadsafe(
|
||||
hass.loop,
|
||||
@@ -292,7 +298,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
EVENT_HOMEASSISTANT_START, open_browser
|
||||
)
|
||||
|
||||
return hass.start()
|
||||
return await hass.async_run()
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
@@ -347,7 +353,20 @@ def main() -> int:
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
attempt_use_uvloop()
|
||||
set_loop()
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
nt_args = cmdline() + ['--runner']
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(nt_args)
|
||||
sys.exit(0)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if exc.returncode != RESTART_EXIT_CODE:
|
||||
sys.exit(exc.returncode)
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
@@ -366,11 +385,12 @@ def main() -> int:
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
exit_code = setup_and_run_hass(config_dir, args)
|
||||
from homeassistant.util.async_ import asyncio_run
|
||||
exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
return exit_code
|
||||
return exit_code # type: ignore # mypy cannot yet infer it
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
import jwt
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -14,6 +16,9 @@ from . import auth_store, models
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
|
||||
EVENT_USER_ADDED = 'user_added'
|
||||
EVENT_USER_REMOVED = 'user_removed'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = Tuple[str, Optional[str]]
|
||||
@@ -124,23 +129,38 @@ class AuthManager:
|
||||
|
||||
async def async_create_system_user(self, name: str) -> models.User:
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
user = await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
groups=[],
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
group = (await self._store.async_get_groups())[0]
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
'groups': [group]
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
user = await self._store.async_create_user(**kwargs)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
||||
-> models.User:
|
||||
@@ -160,12 +180,18 @@ class AuthManager:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
|
||||
return await self._store.async_create_user(
|
||||
user = await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.name,
|
||||
is_active=info.is_active,
|
||||
)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_ADDED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
return user
|
||||
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
"""Link credentials to an existing user."""
|
||||
@@ -183,6 +209,10 @@ class AuthManager:
|
||||
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_USER_REMOVED, {
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
async def async_activate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
@@ -242,8 +272,12 @@ class AuthManager:
|
||||
modules[module_id] = module.name
|
||||
return modules
|
||||
|
||||
async def async_create_refresh_token(self, user: models.User,
|
||||
client_id: Optional[str] = None) \
|
||||
async def async_create_refresh_token(
|
||||
self, user: models.User, client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: Optional[str] = None,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
||||
-> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
@@ -254,10 +288,36 @@ class AuthManager:
|
||||
'System generated users cannot have refresh tokens connected '
|
||||
'to a client.')
|
||||
|
||||
if not user.system_generated and client_id is None:
|
||||
if token_type is None:
|
||||
if user.system_generated:
|
||||
token_type = models.TOKEN_TYPE_SYSTEM
|
||||
else:
|
||||
token_type = models.TOKEN_TYPE_NORMAL
|
||||
|
||||
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
||||
raise ValueError(
|
||||
'System generated users can only have system type '
|
||||
'refresh tokens')
|
||||
|
||||
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
|
||||
raise ValueError('Client is required to generate a refresh token.')
|
||||
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
|
||||
client_name is None):
|
||||
raise ValueError('Client_name is required for long-lived access '
|
||||
'token')
|
||||
|
||||
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
|
||||
for token in user.refresh_tokens.values():
|
||||
if (token.client_name == client_name and token.token_type ==
|
||||
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
|
||||
# Each client_name can only have one
|
||||
# long_lived_access_token type of refresh token
|
||||
raise ValueError('{} already exists'.format(client_name))
|
||||
|
||||
return await self._store.async_create_refresh_token(
|
||||
user, client_id, client_name, client_icon,
|
||||
token_type, access_token_expiration)
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
||||
@@ -277,13 +337,17 @@ class AuthManager:
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self,
|
||||
refresh_token: models.RefreshToken) -> str:
|
||||
refresh_token: models.RefreshToken,
|
||||
remote_ip: Optional[str] = None) -> str:
|
||||
"""Create a new access token."""
|
||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
now = dt_util.utcnow()
|
||||
return jwt.encode({
|
||||
'iss': refresh_token.id,
|
||||
'iat': dt_util.utcnow(),
|
||||
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
|
||||
'iat': now,
|
||||
'exp': now + refresh_token.access_token_expiration,
|
||||
}, refresh_token.jwt_key, algorithm='HS256').decode()
|
||||
|
||||
async def async_validate_access_token(
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"""Storage for auth models."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import hmac
|
||||
from logging import getLogger
|
||||
from typing import Any, Dict, List, Optional # noqa: F401
|
||||
import hmac
|
||||
|
||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
from .permissions import DEFAULT_POLICY
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
INITIAL_GROUP_NAME = 'All Access'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
@@ -27,7 +30,17 @@ class AuthStore:
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None # type: Optional[Dict[str, models.User]]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._groups = None # type: Optional[Dict[str, models.Group]]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
|
||||
async def async_get_groups(self) -> List[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
assert self._groups is not None
|
||||
|
||||
return list(self._groups.values())
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
@@ -49,14 +62,20 @@ class AuthStore:
|
||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
groups: Optional[List[models.Group]] = None) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
'name': name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups or [],
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
@@ -128,11 +147,27 @@ class AuthStore:
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_create_refresh_token(
|
||||
self, user: models.User, client_id: Optional[str] = None) \
|
||||
self, user: models.User, client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
||||
-> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = models.RefreshToken(user=user, client_id=client_id)
|
||||
kwargs = {
|
||||
'user': user,
|
||||
'client_id': client_id,
|
||||
'token_type': token_type,
|
||||
'access_token_expiration': access_token_expiration
|
||||
} # type: Dict[str, Any]
|
||||
if client_name:
|
||||
kwargs['client_name'] = client_name
|
||||
if client_icon:
|
||||
kwargs['client_icon'] = client_icon
|
||||
|
||||
refresh_token = models.RefreshToken(**kwargs)
|
||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||
|
||||
self._async_schedule_save()
|
||||
return refresh_token
|
||||
|
||||
@@ -178,6 +213,15 @@ class AuthStore:
|
||||
|
||||
return found
|
||||
|
||||
@callback
|
||||
def async_log_refresh_token_usage(
|
||||
self, refresh_token: models.RefreshToken,
|
||||
remote_ip: Optional[str] = None) -> None:
|
||||
"""Update refresh token last used information."""
|
||||
refresh_token.last_used_at = dt_util.utcnow()
|
||||
refresh_token.last_used_ip = remote_ip
|
||||
self._async_schedule_save()
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the users."""
|
||||
data = await self._store.async_load()
|
||||
@@ -187,14 +231,45 @@ class AuthStore:
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
if data is None:
|
||||
self._users = users
|
||||
self._set_defaults()
|
||||
return
|
||||
|
||||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
|
||||
# When creating objects we mention each attribute explicetely. This
|
||||
# prevents crashing if user rolls back HA version after a new property
|
||||
# was added.
|
||||
|
||||
for group_dict in data.get('groups', []):
|
||||
groups[group_dict['id']] = models.Group(
|
||||
name=group_dict['name'],
|
||||
id=group_dict['id'],
|
||||
policy=group_dict.get('policy', DEFAULT_POLICY),
|
||||
)
|
||||
|
||||
migrate_group = None
|
||||
|
||||
if not groups:
|
||||
migrate_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY
|
||||
)
|
||||
groups[migrate_group.id] = migrate_group
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(**user_dict)
|
||||
users[user_dict['id']] = models.User(
|
||||
name=user_dict['name'],
|
||||
groups=[groups[group_id] for group_id
|
||||
in user_dict.get('group_ids', [])],
|
||||
id=user_dict['id'],
|
||||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
)
|
||||
if migrate_group is not None and not user_dict['system_generated']:
|
||||
users[user_dict['id']].groups = [migrate_group]
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
@@ -216,18 +291,40 @@ class AuthStore:
|
||||
'Ignoring refresh token %(id)s with invalid created_at '
|
||||
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
||||
continue
|
||||
|
||||
token_type = rt_dict.get('token_type')
|
||||
if token_type is None:
|
||||
if rt_dict['client_id'] is None:
|
||||
token_type = models.TOKEN_TYPE_SYSTEM
|
||||
else:
|
||||
token_type = models.TOKEN_TYPE_NORMAL
|
||||
|
||||
# old refresh_token don't have last_used_at (pre-0.78)
|
||||
last_used_at_str = rt_dict.get('last_used_at')
|
||||
if last_used_at_str:
|
||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||
else:
|
||||
last_used_at = None
|
||||
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict['id'],
|
||||
user=users[rt_dict['user_id']],
|
||||
client_id=rt_dict['client_id'],
|
||||
# use dict.get to keep backward compatibility
|
||||
client_name=rt_dict.get('client_name'),
|
||||
client_icon=rt_dict.get('client_icon'),
|
||||
token_type=token_type,
|
||||
created_at=created_at,
|
||||
access_token_expiration=timedelta(
|
||||
seconds=rt_dict['access_token_expiration']),
|
||||
token=rt_dict['token'],
|
||||
jwt_key=rt_dict['jwt_key']
|
||||
jwt_key=rt_dict['jwt_key'],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get('last_used_ip'),
|
||||
)
|
||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||
|
||||
self._groups = groups
|
||||
self._users = users
|
||||
|
||||
@callback
|
||||
@@ -242,10 +339,12 @@ class AuthStore:
|
||||
def _data_to_save(self) -> Dict:
|
||||
"""Return the data to store."""
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'group_ids': [group.id for group in user.groups],
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
@@ -254,6 +353,18 @@ class AuthStore:
|
||||
for user in self._users.values()
|
||||
]
|
||||
|
||||
groups = []
|
||||
for group in self._groups.values():
|
||||
g_dict = {
|
||||
'name': group.name,
|
||||
'id': group.id,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if group.policy is not DEFAULT_POLICY:
|
||||
g_dict['policy'] = group.policy
|
||||
|
||||
groups.append(g_dict)
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
@@ -271,11 +382,18 @@ class AuthStore:
|
||||
'id': refresh_token.id,
|
||||
'user_id': user.id,
|
||||
'client_id': refresh_token.client_id,
|
||||
'client_name': refresh_token.client_name,
|
||||
'client_icon': refresh_token.client_icon,
|
||||
'token_type': refresh_token.token_type,
|
||||
'created_at': refresh_token.created_at.isoformat(),
|
||||
'access_token_expiration':
|
||||
refresh_token.access_token_expiration.total_seconds(),
|
||||
'token': refresh_token.token,
|
||||
'jwt_key': refresh_token.jwt_key,
|
||||
'last_used_at':
|
||||
refresh_token.last_used_at.isoformat()
|
||||
if refresh_token.last_used_at else None,
|
||||
'last_used_ip': refresh_token.last_used_ip,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
@@ -283,6 +401,22 @@ class AuthStore:
|
||||
|
||||
return {
|
||||
'users': users,
|
||||
'groups': groups,
|
||||
'credentials': credentials,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
||||
def _set_defaults(self) -> None:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
# Add default group
|
||||
all_access_group = models.Group(
|
||||
name=INITIAL_GROUP_NAME,
|
||||
policy=DEFAULT_POLICY,
|
||||
)
|
||||
|
||||
groups = OrderedDict() # type: Dict[str, models.Group]
|
||||
groups[all_access_group.id] = all_access_group
|
||||
|
||||
self._groups = groups
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
from datetime import timedelta
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Plugable auth modules for Home Assistant."""
|
||||
from datetime import timedelta
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
@@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,6 +31,7 @@ class MultiFactorAuthModule:
|
||||
"""Multi-factor Auth Module of validation function."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth module'
|
||||
MAX_RETRY_TIME = 3
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize an auth module."""
|
||||
@@ -84,7 +82,7 @@ class MultiFactorAuthModule:
|
||||
"""Return whether user is setup."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_validation(
|
||||
async def async_validate(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_validation(
|
||||
async def async_validate(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
for data in self._data:
|
||||
|
||||
325
homeassistant/auth/mfa_modules/notify.py
Normal file
325
homeassistant/auth/mfa_modules/notify.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""HMAC-based One-time Password auth module.
|
||||
|
||||
Sending HOTP through notify service
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
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']
|
||||
|
||||
CONF_MESSAGE = 'message'
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MESSAGE,
|
||||
default='{} is your Home Assistant login code'): str
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_module.notify'
|
||||
STORAGE_USERS = 'users'
|
||||
STORAGE_USER_ID = 'user_id'
|
||||
|
||||
INPUT_FIELD_CODE = 'code'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _generate_secret() -> str:
|
||||
"""Generate a secret."""
|
||||
import pyotp
|
||||
return str(pyotp.random_base32())
|
||||
|
||||
|
||||
def _generate_random() -> int:
|
||||
"""Generate a 8 digit number."""
|
||||
import pyotp
|
||||
return int(pyotp.random_base32(length=8, chars=list('1234567890')))
|
||||
|
||||
|
||||
def _generate_otp(secret: str, count: int) -> str:
|
||||
"""Generate one time password."""
|
||||
import pyotp
|
||||
return str(pyotp.HOTP(secret).at(count))
|
||||
|
||||
|
||||
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||
"""Verify one time password."""
|
||||
import pyotp
|
||||
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class NotifySetting:
|
||||
"""Store notify setting for one user."""
|
||||
|
||||
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
|
||||
counter = attr.ib(type=int, factory=_generate_random) # not persistent
|
||||
notify_service = attr.ib(type=Optional[str], default=None)
|
||||
target = attr.ib(type=Optional[str], default=None)
|
||||
|
||||
|
||||
_UsersDict = Dict[str, NotifySetting]
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('notify')
|
||||
class NotifyAuthModule(MultiFactorAuthModule):
|
||||
"""Auth module send hmac-based one time password by notify service."""
|
||||
|
||||
DEFAULT_TITLE = 'Notify One-Time Password'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._user_settings = None # type: Optional[_UsersDict]
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||
self._include = config.get(CONF_INCLUDE, [])
|
||||
self._exclude = config.get(CONF_EXCLUDE, [])
|
||||
self._message_template = config[CONF_MESSAGE]
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({INPUT_FIELD_CODE: str})
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._user_settings = {
|
||||
user_id: NotifySetting(**setting)
|
||||
for user_id, setting in data.get(STORAGE_USERS, {}).items()
|
||||
}
|
||||
|
||||
async def _async_save(self) -> None:
|
||||
"""Save data."""
|
||||
if self._user_settings is None:
|
||||
return
|
||||
|
||||
await self._user_store.async_save({STORAGE_USERS: {
|
||||
user_id: attr.asdict(
|
||||
notify_setting, filter=attr.filters.exclude(
|
||||
attr.fields(NotifySetting).secret,
|
||||
attr.fields(NotifySetting).counter,
|
||||
))
|
||||
for user_id, notify_setting
|
||||
in self._user_settings.items()
|
||||
}})
|
||||
|
||||
@callback
|
||||
def aync_get_available_notify_services(self) -> List[str]:
|
||||
"""Return list of notify services."""
|
||||
unordered_services = set()
|
||||
|
||||
for service in self.hass.services.async_services().get('notify', {}):
|
||||
if service not in self._exclude:
|
||||
unordered_services.add(service)
|
||||
|
||||
if self._include:
|
||||
unordered_services &= set(self._include)
|
||||
|
||||
return sorted(unordered_services)
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
"""
|
||||
return NotifySetupFlow(
|
||||
self, self.input_schema, user_id,
|
||||
self.aync_get_available_notify_services())
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up auth module for user."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
self._user_settings[user_id] = NotifySetting(
|
||||
notify_service=setup_data.get('notify_service'),
|
||||
target=setup_data.get('target'),
|
||||
)
|
||||
|
||||
await self._async_save()
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
if self._user_settings.pop(user_id, None):
|
||||
await self._async_save()
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
return user_id in self._user_settings
|
||||
|
||||
async def async_validate(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id, None)
|
||||
if notify_setting is None:
|
||||
return False
|
||||
|
||||
# user_input has been validate in caller
|
||||
return await self.hass.async_add_executor_job(
|
||||
_verify_otp, notify_setting.secret,
|
||||
user_input.get(INPUT_FIELD_CODE, ''),
|
||||
notify_setting.counter)
|
||||
|
||||
async def async_initialize_login_mfa_step(self, user_id: str) -> None:
|
||||
"""Generate code and notify user."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id, None)
|
||||
if notify_setting is None:
|
||||
raise ValueError('Cannot find user_id')
|
||||
|
||||
def generate_secret_and_one_time_password() -> str:
|
||||
"""Generate and send one time password."""
|
||||
assert notify_setting
|
||||
# secret and counter are not persistent
|
||||
notify_setting.secret = _generate_secret()
|
||||
notify_setting.counter = _generate_random()
|
||||
return _generate_otp(
|
||||
notify_setting.secret, notify_setting.counter)
|
||||
|
||||
code = await self.hass.async_add_executor_job(
|
||||
generate_secret_and_one_time_password)
|
||||
|
||||
await self.async_notify_user(user_id, code)
|
||||
|
||||
async def async_notify_user(self, user_id: str, code: str) -> None:
|
||||
"""Send code by user's notify service."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id, None)
|
||||
if notify_setting is None:
|
||||
_LOGGER.error('Cannot find user %s', user_id)
|
||||
return
|
||||
|
||||
await self.async_notify( # type: ignore
|
||||
code, notify_setting.notify_service, notify_setting.target)
|
||||
|
||||
async def async_notify(self, code: str, notify_service: str,
|
||||
target: Optional[str] = None) -> None:
|
||||
"""Send code by notify service."""
|
||||
data = {'message': self._message_template.format(code)}
|
||||
if target:
|
||||
data['target'] = [target]
|
||||
|
||||
await self.hass.services.async_call('notify', notify_service, data)
|
||||
|
||||
|
||||
class NotifySetupFlow(SetupFlow):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: NotifyAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user_id: str,
|
||||
available_notify_services: List[str]) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user_id)
|
||||
# to fix typing complaint
|
||||
self._auth_module = auth_module # type: NotifyAuthModule
|
||||
self._available_notify_services = available_notify_services
|
||||
self._secret = None # type: Optional[str]
|
||||
self._count = None # type: Optional[int]
|
||||
self._notify_service = None # type: Optional[str]
|
||||
self._target = None # type: Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Let user select available notify services."""
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
hass = self._auth_module.hass
|
||||
if user_input:
|
||||
self._notify_service = user_input['notify_service']
|
||||
self._target = user_input.get('target')
|
||||
self._secret = await hass.async_add_executor_job(_generate_secret)
|
||||
self._count = await hass.async_add_executor_job(_generate_random)
|
||||
|
||||
return await self.async_step_setup()
|
||||
|
||||
if not self._available_notify_services:
|
||||
return self.async_abort(reason='no_available_service')
|
||||
|
||||
schema = OrderedDict() # type: Dict[str, Any]
|
||||
schema['notify_service'] = vol.In(self._available_notify_services)
|
||||
schema['target'] = vol.Optional(str)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
async def async_step_setup(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Verify user can recevie one-time password."""
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
hass = self._auth_module.hass
|
||||
if user_input:
|
||||
verified = await hass.async_add_executor_job(
|
||||
_verify_otp, self._secret, user_input['code'], self._count)
|
||||
if verified:
|
||||
await self._auth_module.async_setup_user(
|
||||
self._user_id, {
|
||||
'notify_service': self._notify_service,
|
||||
'target': self._target,
|
||||
})
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={}
|
||||
)
|
||||
|
||||
errors['base'] = 'invalid_code'
|
||||
|
||||
# generate code every time, no retry logic
|
||||
assert self._secret and self._count
|
||||
code = await hass.async_add_executor_job(
|
||||
_generate_otp, self._secret, self._count)
|
||||
|
||||
assert self._notify_service
|
||||
await self._auth_module.async_notify(
|
||||
code, self._notify_service, self._target)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='setup',
|
||||
data_schema=self._setup_schema,
|
||||
description_placeholders={'notify_service': self._notify_service},
|
||||
errors=errors,
|
||||
)
|
||||
@@ -60,13 +60,14 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Auth module validate time-based one time password."""
|
||||
|
||||
DEFAULT_TITLE = 'Time-based One Time Password'
|
||||
MAX_RETRY_TIME = 5
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._users = None # type: Optional[Dict[str, str]]
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY)
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
@@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validation(
|
||||
async def async_validate(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
@@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
if ota_secret is None:
|
||||
# even we cannot find user, we still do verify
|
||||
# to make timing the same as if user was found.
|
||||
pyotp.TOTP(DUMMY_SECRET).verify(code)
|
||||
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
||||
return False
|
||||
|
||||
return bool(pyotp.TOTP(ota_secret).verify(code))
|
||||
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
|
||||
|
||||
|
||||
class TotpSetupFlow(SetupFlow):
|
||||
|
||||
@@ -7,45 +7,88 @@ import attr
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ACCESS_TOKEN_EXPIRATION
|
||||
from . import permissions as perm_mdl
|
||||
from .util import generate_secret
|
||||
|
||||
TOKEN_TYPE_NORMAL = 'normal'
|
||||
TOKEN_TYPE_SYSTEM = 'system'
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Group:
|
||||
"""A group."""
|
||||
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
policy = attr.ib(type=perm_mdl.PolicyType)
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(
|
||||
type=list, default=attr.Factory(list), cmp=False
|
||||
type=list, factory=list, cmp=False
|
||||
) # type: List[Credentials]
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(
|
||||
type=dict, default=attr.Factory(dict), cmp=False
|
||||
type=dict, factory=dict, cmp=False
|
||||
) # type: Dict[str, RefreshToken]
|
||||
|
||||
_permissions = attr.ib(
|
||||
type=perm_mdl.PolicyPermissions,
|
||||
init=False,
|
||||
cmp=False,
|
||||
default=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def permissions(self) -> perm_mdl.AbstractPermissions:
|
||||
"""Return permissions object for user."""
|
||||
if self.is_owner:
|
||||
return perm_mdl.OwnerPermissions
|
||||
|
||||
if self._permissions is not None:
|
||||
return self._permissions
|
||||
|
||||
self._permissions = perm_mdl.PolicyPermissions(
|
||||
perm_mdl.merge_policies([
|
||||
group.policy for group in self.groups]))
|
||||
|
||||
return self._permissions
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str) # type: Optional[str]
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
access_token_expiration = attr.ib(type=timedelta,
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
jwt_key = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
client_id = attr.ib(type=Optional[str])
|
||||
access_token_expiration = attr.ib(type=timedelta)
|
||||
client_name = attr.ib(type=Optional[str], default=None)
|
||||
client_icon = attr.ib(type=Optional[str], default=None)
|
||||
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
|
||||
validator=attr.validators.in_((
|
||||
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
created_at = attr.ib(type=datetime, factory=dt_util.utcnow)
|
||||
token = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||
jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||
|
||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@@ -53,12 +96,12 @@ class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str) # type: Optional[str]
|
||||
auth_provider_id = attr.ib(type=Optional[str])
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
|
||||
252
homeassistant/auth/permissions.py
Normal file
252
homeassistant/auth/permissions.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from typing import ( # noqa: F401
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State
|
||||
|
||||
CategoryType = Union[Mapping[str, 'CategoryType'], bool, None]
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
||||
|
||||
# Default policy if group has no policy applied.
|
||||
DEFAULT_POLICY = {
|
||||
"entities": True
|
||||
} # type: PolicyType
|
||||
|
||||
CAT_ENTITIES = 'entities'
|
||||
ENTITY_DOMAINS = 'domains'
|
||||
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||
|
||||
VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||
str: True
|
||||
}))
|
||||
|
||||
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(ENTITY_DOMAINS): VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_ENTITY_IDS): VALUES_SCHEMA,
|
||||
}))
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
raise NotImplementedError
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PolicyPermissions(AbstractPermissions):
|
||||
"""Handle permissions."""
|
||||
|
||||
def __init__(self, policy: PolicyType) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._compiled = {} # type: Dict[str, Callable[..., bool]]
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
return func(entity_id, keys)
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
func = self._policy_func(CAT_ENTITIES, _compile_entities)
|
||||
keys = ('read',)
|
||||
return [entity for entity in states if func(entity.entity_id, keys)]
|
||||
|
||||
def _policy_func(self, category: str,
|
||||
compile_func: Callable[[CategoryType], Callable]) \
|
||||
-> Callable[..., bool]:
|
||||
"""Get a policy function."""
|
||||
func = self._compiled.get(category)
|
||||
|
||||
if func:
|
||||
return func
|
||||
|
||||
func = self._compiled[category] = compile_func(
|
||||
self._policy.get(category))
|
||||
return func
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
# pylint: disable=protected-access
|
||||
return (isinstance(other, PolicyPermissions) and
|
||||
other._policy == self._policy)
|
||||
|
||||
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def check_entity(self, entity_id: str, *keys: str) -> bool:
|
||||
"""Test if we can access entity."""
|
||||
return True
|
||||
|
||||
def filter_states(self, states: List[State]) -> List[State]:
|
||||
"""Filter a list of states for what the user is allowed to see."""
|
||||
return states
|
||||
|
||||
|
||||
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _compile_entities(policy: CategoryType) \
|
||||
-> Callable[[str, Tuple[str]], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
if not policy:
|
||||
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all
|
||||
|
||||
if policy is True:
|
||||
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Approve all."""
|
||||
return True
|
||||
|
||||
return apply_policy_allow_all
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
domains = policy.get(ENTITY_DOMAINS)
|
||||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
|
||||
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
|
||||
|
||||
# The order of these functions matter. The more precise are at the top.
|
||||
# If a function returns None, they cannot handle it.
|
||||
# If a function returns a boolean, that's the result to return.
|
||||
|
||||
# Setting entity_ids to a boolean is final decision for permissions
|
||||
# So return right away.
|
||||
if isinstance(entity_ids, bool):
|
||||
def apply_entity_id_policy(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids # type: ignore
|
||||
|
||||
return apply_entity_id_policy
|
||||
|
||||
if entity_ids is not None:
|
||||
def allowed_entity_id(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed entity_id."""
|
||||
return entity_ids.get(entity_id) # type: ignore
|
||||
|
||||
funcs.append(allowed_entity_id)
|
||||
|
||||
if isinstance(domains, bool):
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
return domains
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
elif domains is not None:
|
||||
def allowed_domain(entity_id: str, keys: Tuple[str]) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed domain."""
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
return domains.get(domain) # type: ignore
|
||||
|
||||
funcs.append(allowed_domain)
|
||||
|
||||
# Can happen if no valid subcategories specified
|
||||
if not funcs:
|
||||
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Decline all."""
|
||||
return False
|
||||
|
||||
return apply_policy_deny_all_2
|
||||
|
||||
if len(funcs) == 1:
|
||||
func = funcs[0]
|
||||
|
||||
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply a single policy function."""
|
||||
return func(entity_id, keys) is True
|
||||
|
||||
return apply_policy_func
|
||||
|
||||
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(entity_id, keys)
|
||||
if result is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
return apply_policy_funcs
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
"""Merge policies."""
|
||||
new_policy = {} # type: Dict[str, CategoryType]
|
||||
seen = set() # type: Set[str]
|
||||
for policy in policies:
|
||||
for category in policy:
|
||||
if category in seen:
|
||||
continue
|
||||
seen.add(category)
|
||||
new_policy[category] = _merge_policies([
|
||||
policy.get(category) for policy in policies])
|
||||
cast(PolicyType, new_policy)
|
||||
return new_policy
|
||||
|
||||
|
||||
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
"""Merge a policy."""
|
||||
# When merging policies, the most permissive wins.
|
||||
# This means we order it like this:
|
||||
# True > Dict > None
|
||||
#
|
||||
# True: allow everything
|
||||
# Dict: specify more granular permissions
|
||||
# None: no opinion
|
||||
#
|
||||
# If there are multiple sources with a dict as policy, we recursively
|
||||
# merge each key in the source.
|
||||
|
||||
policy = None # type: CategoryType
|
||||
seen = set() # type: Set[str]
|
||||
for source in sources:
|
||||
if source is None:
|
||||
continue
|
||||
|
||||
# A source that's True will always win. Shortcut return.
|
||||
if source is True:
|
||||
return True
|
||||
|
||||
assert isinstance(source, dict)
|
||||
|
||||
if policy is None:
|
||||
policy = {}
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
for key in source:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
key_sources = []
|
||||
for src in sources:
|
||||
if isinstance(src, dict):
|
||||
key_sources.append(src.get(key))
|
||||
|
||||
policy[key] = _merge_policies(key_sources)
|
||||
|
||||
return policy
|
||||
@@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from ..auth_store import AuthStore
|
||||
from ..const import MFA_SESSION_EXPIRATION
|
||||
from ..models import Credentials, User, UserMeta # noqa: F401
|
||||
from ..mfa_modules import SESSION_EXPIRATION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = 'auth_prov_reqs_processed'
|
||||
@@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
||||
self.available_mfa_modules = {} # type: Dict[str, str]
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.invalid_mfa_times = 0
|
||||
self.user = None # type: Optional[User]
|
||||
|
||||
async def async_step_init(
|
||||
@@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
assert self.user
|
||||
|
||||
errors = {}
|
||||
|
||||
auth_module = self._auth_manager.get_auth_mfa_module(
|
||||
@@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
# will show invalid_auth_module error
|
||||
return await self.async_step_select_mfa_module(user_input={})
|
||||
|
||||
if user_input is None and hasattr(auth_module,
|
||||
'async_initialize_login_mfa_step'):
|
||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
||||
|
||||
if user_input is not None:
|
||||
expires = self.created_at + SESSION_EXPIRATION
|
||||
expires = self.created_at + MFA_SESSION_EXPIRATION
|
||||
if dt_util.utcnow() > expires:
|
||||
return self.async_abort(
|
||||
reason='login_expired'
|
||||
)
|
||||
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
result = await auth_module.async_validate(
|
||||
self.user.id, user_input)
|
||||
if not result:
|
||||
errors['base'] = 'invalid_code'
|
||||
self.invalid_mfa_times += 1
|
||||
if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0:
|
||||
return self.async_abort(
|
||||
reason='too_many_retry'
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
|
||||
description_placeholders = {
|
||||
'mfa_module_name': auth_module.name,
|
||||
'mfa_module_id': auth_module.id
|
||||
} # type: Dict[str, str]
|
||||
'mfa_module_id': auth_module.id,
|
||||
} # type: Dict[str, Optional[str]]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='mfa',
|
||||
|
||||
@@ -52,7 +52,8 @@ class Data:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the user data store."""
|
||||
self.hass = hass
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
self._data = None # type: Optional[Dict[str, Any]]
|
||||
|
||||
async def async_load(self) -> None:
|
||||
|
||||
@@ -24,7 +24,7 @@ USER_SCHEMA = vol.Schema({
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
LEGACY_USER = 'homeassistant'
|
||||
LEGACY_USER_NAME = 'Legacy API password user'
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
@@ -52,23 +52,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Return LEGACY_USER always."""
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == LEGACY_USER:
|
||||
return credential
|
||||
"""Return credentials for this login."""
|
||||
credentials = await self.async_credentials()
|
||||
if credentials:
|
||||
return credentials[0]
|
||||
|
||||
return self.async_create_credentials({
|
||||
'username': LEGACY_USER
|
||||
})
|
||||
return self.async_create_credentials({})
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""
|
||||
Set name as LEGACY_USER always.
|
||||
Return info for the user.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return UserMeta(name=LEGACY_USER, is_active=True)
|
||||
return UserMeta(name=LEGACY_USER_NAME, is_active=True)
|
||||
|
||||
|
||||
class LegacyLoginFlow(LoginFlow):
|
||||
|
||||
@@ -5,7 +5,6 @@ import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -19,7 +18,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.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import itertools as it
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -21,11 +23,16 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
SERVICE_CHECK_CONFIG = 'check_config'
|
||||
SERVICE_UPDATE_ENTITY = 'update_entity'
|
||||
SCHEMA_UPDATE_ENTITY = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_id
|
||||
})
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
@@ -59,61 +66,9 @@ def is_on(hass, entity_id=None):
|
||||
return False
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None, **service_data):
|
||||
"""Turn specified entity on if possible."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data)
|
||||
|
||||
|
||||
def turn_off(hass, entity_id=None, **service_data):
|
||||
"""Turn specified entity off."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
||||
|
||||
|
||||
def toggle(hass, entity_id=None, **service_data):
|
||||
"""Toggle specified entity."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def stop(hass):
|
||||
"""Stop Home Assistant."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP)
|
||||
|
||||
|
||||
def restart(hass):
|
||||
"""Stop Home Assistant."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART)
|
||||
|
||||
|
||||
def check_config(hass):
|
||||
"""Check the config files."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
"""Set up general services related to Home Assistant."""
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
async def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
|
||||
@@ -148,7 +103,7 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
@@ -164,15 +119,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
async def async_handle_core_service(call):
|
||||
"""Service handler for handling core services."""
|
||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||
hass.async_create_task(hass.async_stop())
|
||||
return
|
||||
|
||||
try:
|
||||
errors = yield from conf_util.async_check_ha_config_file(hass)
|
||||
errors = await conf_util.async_check_ha_config_file(hass)
|
||||
except HomeAssistantError:
|
||||
return
|
||||
|
||||
@@ -186,23 +140,30 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
async def async_handle_update_service(call):
|
||||
"""Service handler for updating an entity."""
|
||||
await hass.helpers.entity_component.async_update_entity(
|
||||
call.data[ATTR_ENTITY_ID])
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service,
|
||||
schema=SCHEMA_UPDATE_ENTITY)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
async def async_handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
try:
|
||||
conf = yield from conf_util.async_hass_config_yaml(hass)
|
||||
conf = await conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
yield from conf_util.async_process_ha_core_config(
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
|
||||
@@ -4,7 +4,6 @@ This component provides basic support for Abode Home Security system.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/abode/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
@@ -19,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.13.1']
|
||||
REQUIREMENTS = ['abodepy==0.14.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -261,8 +260,7 @@ class AbodeDevice(Entity):
|
||||
self._data = data
|
||||
self._device = device
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_device_callback,
|
||||
@@ -308,8 +306,7 @@ class AbodeAutomation(Entity):
|
||||
self._automation = automation
|
||||
self._event = event
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
if self._event:
|
||||
self.hass.async_add_job(
|
||||
|
||||
@@ -4,7 +4,6 @@ Component to interface with an alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -14,7 +13,6 @@ from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -32,85 +30,12 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm home."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm away."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_night(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm night."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_trigger(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for trigger."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm custom bypass."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -59,8 +58,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
self._ready = None
|
||||
self._zone_bypassed = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@@ -4,7 +4,6 @@ Interfaces with Alarm.com alarm control panels.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
@@ -42,7 +40,7 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||
yield from alarmdotcom.async_login()
|
||||
await alarmdotcom.async_login()
|
||||
async_add_entities([alarmdotcom])
|
||||
|
||||
|
||||
@@ -63,15 +61,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
async def async_login(self):
|
||||
"""Login to Alarm.com."""
|
||||
yield from self._alarm.async_login()
|
||||
await self._alarm.async_login()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Fetch the latest state."""
|
||||
yield from self._alarm.async_update()
|
||||
await self._alarm.async_update()
|
||||
return self._alarm.state
|
||||
|
||||
@property
|
||||
@@ -106,23 +102,20 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
'sensor_status': self._alarm.sensor_status
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_disarm()
|
||||
await self._alarm.async_alarm_disarm()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm hom command."""
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_home()
|
||||
await self._alarm.async_alarm_arm_home()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_away()
|
||||
await self._alarm.async_alarm_arm_away()
|
||||
|
||||
def _validate_code(self, code):
|
||||
"""Validate given code."""
|
||||
|
||||
92
homeassistant/components/alarm_control_panel/blink.py
Normal file
92
homeassistant/components/alarm_control_panel/blink.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Blink Alarm Control Panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.blink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.blink import (
|
||||
BLINK_DATA, DEFAULT_ATTRIBUTION)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
ICON = 'mdi:security'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
data = hass.data[BLINK_DATA]
|
||||
|
||||
# Current version of blinkpy API only supports one sync module. When
|
||||
# support for additional models is added, the sync module name should
|
||||
# come from the API.
|
||||
sync_modules = []
|
||||
sync_modules.append(BlinkSyncModule(data, 'sync'))
|
||||
add_entities(sync_modules, True)
|
||||
|
||||
|
||||
class BlinkSyncModule(AlarmControlPanel):
|
||||
"""Representation of a Blink Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, name):
|
||||
"""Initialize the alarm control panel."""
|
||||
self.data = data
|
||||
self.sync = data.sync
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for the sync module."""
|
||||
return self.sync.serial
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the panel."""
|
||||
return "{} {}".format(BLINK_DATA, self._name)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = self.sync.attributes
|
||||
attr['network_info'] = self.data.networks
|
||||
attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
_LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name)
|
||||
self.data.refresh()
|
||||
mode = self.sync.arm
|
||||
if mode:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self.sync.arm = False
|
||||
self.sync.refresh()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm command."""
|
||||
self.sync.arm = True
|
||||
self.sync.refresh()
|
||||
@@ -4,7 +4,6 @@ Interfaces with Egardia/Woonveilig alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.egardia/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import requests
|
||||
@@ -61,8 +60,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
self._rs_codes = rs_codes
|
||||
self._rs_port = rs_port
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Add Egardiaserver callback if enabled."""
|
||||
if self._rs_enabled:
|
||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
||||
|
||||
204
homeassistant/components/alarm_control_panel/elkm1.py
Normal file
204
homeassistant/components/alarm_control_panel/elkm1.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
Each ElkM1 area will be created as a separate alarm_control_panel in HASS.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.elkm1/
|
||||
"""
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.elkm1 import (
|
||||
DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
|
||||
DEPENDENCIES = [ELK_DOMAIN]
|
||||
|
||||
SIGNAL_ARM_ENTITY = 'elkm1_arm'
|
||||
SIGNAL_DISPLAY_MESSAGE = 'elkm1_display_message'
|
||||
|
||||
ELK_ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
|
||||
vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)),
|
||||
})
|
||||
|
||||
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
|
||||
vol.Optional('clear', default=2): vol.In([0, 1, 2]),
|
||||
vol.Optional('beep', default=False): cv.boolean,
|
||||
vol.Optional('timeout', default=0): vol.Range(min=0, max=65535),
|
||||
vol.Optional('line1', default=''): cv.string,
|
||||
vol.Optional('line2', default=''): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the ElkM1 alarm platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
elk = hass.data[ELK_DOMAIN]['elk']
|
||||
entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, [])
|
||||
async_add_entities(entities, True)
|
||||
|
||||
def _dispatch(signal, entity_ids, *args):
|
||||
for entity_id in entity_ids:
|
||||
async_dispatcher_send(
|
||||
hass, '{}_{}'.format(signal, entity_id), *args)
|
||||
|
||||
def _arm_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
arm_level = _arm_services().get(service.service)
|
||||
args = (arm_level, service.data.get(ATTR_CODE))
|
||||
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
|
||||
|
||||
for service in _arm_services():
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA)
|
||||
|
||||
def _display_message_service(service):
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
|
||||
data = service.data
|
||||
args = (data['clear'], data['beep'], data['timeout'],
|
||||
data['line1'], data['line2'])
|
||||
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, 'elkm1_alarm_display_message',
|
||||
_display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
def _arm_services():
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
return {
|
||||
'elkm1_alarm_arm_vacation': ArmLevel.ARMED_VACATION.value,
|
||||
'elkm1_alarm_arm_home_instant': ArmLevel.ARMED_STAY_INSTANT.value,
|
||||
'elkm1_alarm_arm_night_instant': ArmLevel.ARMED_NIGHT_INSTANT.value,
|
||||
}
|
||||
|
||||
|
||||
class ElkArea(ElkEntity, alarm.AlarmControlPanel):
|
||||
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
|
||||
|
||||
def __init__(self, element, elk, elk_data):
|
||||
"""Initialize Area as Alarm Control Panel."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
self._changed_by_entity_id = ''
|
||||
self._state = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callback for ElkM1 changes."""
|
||||
await super().async_added_to_hass()
|
||||
for keypad in self._elk.keypads:
|
||||
keypad.add_callback(self._watch_keypad)
|
||||
async_dispatcher_connect(
|
||||
self.hass, '{}_{}'.format(SIGNAL_ARM_ENTITY, self.entity_id),
|
||||
self._arm_service)
|
||||
async_dispatcher_connect(
|
||||
self.hass, '{}_{}'.format(SIGNAL_DISPLAY_MESSAGE, self.entity_id),
|
||||
self._display_message)
|
||||
|
||||
def _watch_keypad(self, keypad, changeset):
|
||||
if keypad.area != self._element.index:
|
||||
return
|
||||
if changeset.get('last_user') is not None:
|
||||
self._changed_by_entity_id = self.hass.data[
|
||||
ELK_DOMAIN]['keypads'].get(keypad.index, '')
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the alarm code format."""
|
||||
return '^[0-9]{4}([0-9]{2})?$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the element."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Attributes of the area."""
|
||||
from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState
|
||||
|
||||
attrs = self.initial_attrs()
|
||||
elmt = self._element
|
||||
attrs['is_exit'] = elmt.is_exit
|
||||
attrs['timer1'] = elmt.timer1
|
||||
attrs['timer2'] = elmt.timer2
|
||||
if elmt.armed_status is not None:
|
||||
attrs['armed_status'] = \
|
||||
ArmedStatus(elmt.armed_status).name.lower()
|
||||
if elmt.arm_up_state is not None:
|
||||
attrs['arm_up_state'] = ArmUpState(elmt.arm_up_state).name.lower()
|
||||
if elmt.alarm_state is not None:
|
||||
attrs['alarm_state'] = AlarmState(elmt.alarm_state).name.lower()
|
||||
attrs['changed_by_entity_id'] = self._changed_by_entity_id
|
||||
return attrs
|
||||
|
||||
def _element_changed(self, element, changeset):
|
||||
from elkm1_lib.const import ArmedStatus
|
||||
|
||||
elk_state_to_hass_state = {
|
||||
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
|
||||
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
|
||||
ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME,
|
||||
ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME,
|
||||
ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT,
|
||||
ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT,
|
||||
ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
if self._element.alarm_state is None:
|
||||
self._state = None
|
||||
elif self._area_is_in_alarm_state():
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
elif self._entry_exit_timer_is_running():
|
||||
self._state = STATE_ALARM_ARMING \
|
||||
if self._element.is_exit else STATE_ALARM_PENDING
|
||||
else:
|
||||
self._state = elk_state_to_hass_state[self._element.armed_status]
|
||||
|
||||
def _entry_exit_timer_is_running(self):
|
||||
return self._element.timer1 > 0 or self._element.timer2 > 0
|
||||
|
||||
def _area_is_in_alarm_state(self):
|
||||
from elkm1_lib.const import AlarmState
|
||||
|
||||
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._element.disarm(int(code))
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
|
||||
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from elkm1_lib.const import ArmLevel
|
||||
|
||||
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
|
||||
|
||||
async def _arm_service(self, arm_level, code):
|
||||
self._element.arm(arm_level, code)
|
||||
|
||||
async def _display_message(self, clear, beep, timeout, line1, line2):
|
||||
"""Display a message on all keypads for the area."""
|
||||
self._element.display_message(clear, beep, timeout, line1, line2)
|
||||
@@ -4,7 +4,6 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -32,9 +31,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
configured_partitions = discovery_info['partitions']
|
||||
code = discovery_info[CONF_CODE]
|
||||
@@ -88,8 +86,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
@@ -128,8 +125,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
@@ -138,8 +134,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
@@ -148,8 +143,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
@@ -158,8 +152,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_trigger(self, code=None):
|
||||
async def async_alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for manual alarms controllable via MQTT.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
@@ -363,8 +362,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||
new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
||||
|
||||
@@ -4,7 +4,6 @@ This platform enables the possibility to control a MQTT alarm.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -18,10 +17,13 @@ from homeassistant.const import (
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
CONF_NAME, CONF_CODE)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
CONF_RETAIN, MqttAvailability)
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_entities, discovery_info=None):
|
||||
"""Set up MQTT alarm control panel through configuration.yaml."""
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up MQTT alarm control panel dynamically through MQTT discovery."""
|
||||
async def async_discover(discovery_payload):
|
||||
"""Discover and add an MQTT alarm control panel."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_payload[ATTR_DISCOVERY_HASH])
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'),
|
||||
async_discover)
|
||||
|
||||
|
||||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
async_add_entities([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
@@ -65,18 +82,22 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
discovery_hash,)])
|
||||
|
||||
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
||||
alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available):
|
||||
availability_topic, payload_available, payload_not_available,
|
||||
discovery_hash):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
@@ -87,11 +108,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
self._discovery_hash = discovery_hash
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events."""
|
||||
yield from super().async_added_to_hass()
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
@@ -104,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self._state = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
yield from mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
@@ -131,8 +153,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -143,8 +164,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -155,8 +175,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command.
|
||||
|
||||
This method is a coroutine.
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ .
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.satel_integra/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
@@ -18,9 +17,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['satel_integra']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up for Satel Integra alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
@@ -39,8 +37,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
self._state = None
|
||||
self._arm_home_mode = arm_home_mode
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
@@ -74,21 +71,18 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].disarm(code)
|
||||
await self.hass.data[DATA_SATEL].disarm(code)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(code)
|
||||
await self.hass.data[DATA_SATEL].arm(code)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(
|
||||
await self.hass.data[DATA_SATEL].arm(
|
||||
code, self._arm_home_mode)
|
||||
|
||||
@@ -79,3 +79,55 @@ ifttt_push_alarm_state:
|
||||
state:
|
||||
description: The state to which the alarm control panel has to be set.
|
||||
example: 'armed_night'
|
||||
|
||||
elkm1_alarm_arm_vacation:
|
||||
description: Arm the ElkM1 in vacation mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_home_instant:
|
||||
description: Arm the ElkM1 in home instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_night_instant:
|
||||
description: Arm the ElkM1 in night instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_display_message:
|
||||
description: Display a message on all of the ElkM1 keypads for an area.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to display messages on.
|
||||
example: 'alarm_control_panel.main'
|
||||
clear:
|
||||
description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2
|
||||
example: 1
|
||||
beep:
|
||||
description: 0=no beep, 1=beep; default 0
|
||||
example: 1
|
||||
timeout:
|
||||
description: Time to display message, 0=forever, max 65535, default 0
|
||||
example: 4242
|
||||
line1:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: The answer to life,
|
||||
line2:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: the universe, and everything.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Interfaces with SimpliSafe alarm control panel.
|
||||
This platform provides alarm control functionality for SimpliSafe.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
@@ -7,80 +7,61 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.simplisafe.const import (
|
||||
DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
||||
CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
ATTR_ALARM_ACTIVE = 'alarm_active'
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up a SimpliSafe alarm control panel based on existing config."""
|
||||
pass
|
||||
|
||||
try:
|
||||
simplisafe = SimpliSafeApiInterface(username, password)
|
||||
except SimpliSafeAPIException:
|
||||
_LOGGER.error("Failed to set up SimpliSafe")
|
||||
return
|
||||
|
||||
systems = []
|
||||
|
||||
for system in simplisafe.get_systems():
|
||||
systems.append(SimpliSafeAlarm(system, name, code))
|
||||
|
||||
add_entities(systems)
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||
systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
async_add_entities([
|
||||
SimpliSafeAlarm(system, entry.data.get(CONF_CODE))
|
||||
for system in systems
|
||||
], True)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, simplisafe, name, code):
|
||||
def __init__(self, system, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
self.simplisafe = simplisafe
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._attrs = {}
|
||||
self._code = code
|
||||
self._system = system
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self.simplisafe.location_id
|
||||
return self._system.system_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
||||
return self._system.address
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
if not self._code:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
@@ -89,53 +70,12 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
status = self.simplisafe.state
|
||||
if status.lower() == 'off':
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status.lower() == 'home' or status.lower() == 'home_count':
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
|
||||
status.lower() == 'away_count'):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
return state
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attributes = {}
|
||||
|
||||
attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active
|
||||
if self.simplisafe.temperature is not None:
|
||||
attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature
|
||||
|
||||
return attributes
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
self.simplisafe.update()
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
_LOGGER.info("SimpliSafe alarm disarming")
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
_LOGGER.info("SimpliSafe alarm arming home")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
_LOGGER.info("SimpliSafe alarm arming away")
|
||||
return self._attrs
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
@@ -143,3 +83,63 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||
if not check:
|
||||
_LOGGER.warning("Wrong code entered for %s", state)
|
||||
return check
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
|
||||
await self._system.set_off()
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
|
||||
await self._system.set_home()
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
|
||||
await self._system.set_away()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update alarm status."""
|
||||
from simplipy.system import SystemStates
|
||||
|
||||
await self._system.update()
|
||||
|
||||
self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off
|
||||
if self._system.temperature:
|
||||
self._attrs[ATTR_TEMPERATURE] = self._system.temperature
|
||||
|
||||
if self._system.state == SystemStates.error:
|
||||
return
|
||||
|
||||
if self._system.state == SystemStates.off:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif self._system.state in (SystemStates.home,
|
||||
SystemStates.home_count):
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif self._system.state in (SystemStates.away, SystemStates.away_count,
|
||||
SystemStates.exit_delay):
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
@@ -4,71 +4,63 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.spc/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.spc import (
|
||||
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_AREA_MODE_TO_STATE = {
|
||||
'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
|
||||
def _get_alarm_state(spc_mode):
|
||||
def _get_alarm_state(area):
|
||||
"""Get the alarm state."""
|
||||
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
|
||||
from pyspcwebgw.const import AreaMode
|
||||
|
||||
if area.verified_alarm:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
mode_to_state = {
|
||||
AreaMode.UNSET: STATE_ALARM_DISARMED,
|
||||
AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
|
||||
AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
|
||||
AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
return mode_to_state.get(area.mode)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
api = hass.data[DATA_API]
|
||||
devices = [SpcAlarm(api, area)
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_entities(devices)
|
||||
async_add_entities([SpcAlarm(area=area, api=api)
|
||||
for area in api.areas.values()])
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of the SPC alarm panel."""
|
||||
|
||||
def __init__(self, api, area):
|
||||
def __init__(self, area, api):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._area_id = area['id']
|
||||
self._name = area['name']
|
||||
self._state = _get_alarm_state(area['mode'])
|
||||
if self._state == STATE_ALARM_DISARMED:
|
||||
self._changed_by = area.get('last_unset_user_name', 'unknown')
|
||||
else:
|
||||
self._changed_by = area.get('last_set_user_name', 'unknown')
|
||||
self._area = area
|
||||
self._api = api
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call for adding new entities."""
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
||||
self._area_id, self)
|
||||
async_dispatcher_connect(self.hass,
|
||||
SIGNAL_UPDATE_ALARM.format(self._area.id),
|
||||
self._update_callback)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
self._changed_by = extra.get('changed_by', 'unknown')
|
||||
self.async_schedule_update_ha_state()
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -78,32 +70,38 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
return self._area.name
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Return the user the last change was triggered by."""
|
||||
return self._changed_by
|
||||
return self._area.last_changed_by
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
return _get_alarm_state(self._area)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.UNSET)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.PART_SET_A)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.PART_SET_B)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
||||
from pyspcwebgw.const import AreaMode
|
||||
await self._api.change_mode(area=self._area,
|
||||
new_mode=AreaMode.FULL_SET)
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.18']
|
||||
REQUIREMENTS = ['total_connect_client==0.20']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Interfaces with Wink Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
@@ -38,8 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation a Wink camera alarm."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
||||
|
||||
|
||||
98
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
Executable file
98
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
Executable file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Yale Smart Alarm client for interacting with the Yale Smart Alarm System API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://www.home-assistant.io/components/alarm_control_panel.yale_smart_alarm
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
|
||||
|
||||
CONF_AREA_ID = 'area_id'
|
||||
|
||||
DEFAULT_NAME = 'Yale Smart Alarm'
|
||||
|
||||
DEFAULT_AREA_ID = '1'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the alarm platform."""
|
||||
name = config[CONF_NAME]
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
area_id = config[CONF_AREA_ID]
|
||||
|
||||
from yalesmartalarmclient.client import (
|
||||
YaleSmartAlarmClient, AuthenticationError)
|
||||
try:
|
||||
client = YaleSmartAlarmClient(username, password, area_id)
|
||||
except AuthenticationError:
|
||||
_LOGGER.error("Authentication failed. Check credentials")
|
||||
return
|
||||
|
||||
add_entities([YaleAlarmDevice(name, client)], True)
|
||||
|
||||
|
||||
class YaleAlarmDevice(AlarmControlPanel):
|
||||
"""Represent a Yale Smart Alarm."""
|
||||
|
||||
def __init__(self, name, client):
|
||||
"""Initialize the Yale Alarm Device."""
|
||||
self._name = name
|
||||
self._client = client
|
||||
self._state = None
|
||||
|
||||
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
|
||||
YALE_STATE_ARM_PARTIAL,
|
||||
YALE_STATE_ARM_FULL)
|
||||
self._state_map = {
|
||||
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
|
||||
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
|
||||
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
armed_status = self._client.get_armed_status()
|
||||
|
||||
self._state = self._state_map.get(armed_status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_partial()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_full()
|
||||
@@ -10,7 +10,8 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY)
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
@@ -59,53 +60,12 @@ def is_on(hass, entity_id):
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_turn_on(hass, entity_id):
|
||||
"""Async reset the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_turn_off(hass, entity_id):
|
||||
"""Async acknowledge the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
hass.add_job(async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_toggle(hass, entity_id):
|
||||
"""Async toggle acknowledgement of alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Alert component."""
|
||||
alerts = config.get(DOMAIN)
|
||||
all_alerts = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_alert_service(service_call):
|
||||
async def async_handle_alert_service(service_call):
|
||||
"""Handle calls to alert services."""
|
||||
alert_ids = service.extract_entity_ids(hass, service_call)
|
||||
|
||||
@@ -113,11 +73,11 @@ def async_setup(hass, config):
|
||||
alert = all_alerts[alert_id]
|
||||
alert.async_set_context(service_call.context)
|
||||
if service_call.service == SERVICE_TURN_ON:
|
||||
yield from alert.async_turn_on()
|
||||
await alert.async_turn_on()
|
||||
elif service_call.service == SERVICE_TOGGLE:
|
||||
yield from alert.async_toggle()
|
||||
await alert.async_toggle()
|
||||
else:
|
||||
yield from alert.async_turn_off()
|
||||
await alert.async_turn_off()
|
||||
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
@@ -141,7 +101,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
@@ -196,17 +156,15 @@ class Alert(ToggleEntity):
|
||||
"""Hide the alert when it is not firing."""
|
||||
return not self._can_ack or not self._firing
|
||||
|
||||
@asyncio.coroutine
|
||||
def watched_entity_change(self, entity, from_state, to_state):
|
||||
async def watched_entity_change(self, entity, from_state, to_state):
|
||||
"""Determine if the alert should start or stop."""
|
||||
_LOGGER.debug("Watched entity (%s) has changed", entity)
|
||||
if to_state.state == self._alert_state and not self._firing:
|
||||
yield from self.begin_alerting()
|
||||
await self.begin_alerting()
|
||||
if to_state.state != self._alert_state and self._firing:
|
||||
yield from self.end_alerting()
|
||||
await self.end_alerting()
|
||||
|
||||
@asyncio.coroutine
|
||||
def begin_alerting(self):
|
||||
async def begin_alerting(self):
|
||||
"""Begin the alert procedures."""
|
||||
_LOGGER.debug("Beginning Alert: %s", self._name)
|
||||
self._ack = False
|
||||
@@ -214,25 +172,23 @@ class Alert(ToggleEntity):
|
||||
self._next_delay = 0
|
||||
|
||||
if not self._skip_first:
|
||||
yield from self._notify()
|
||||
await self._notify()
|
||||
else:
|
||||
yield from self._schedule_notify()
|
||||
await self._schedule_notify()
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
async def end_alerting(self):
|
||||
"""End the alert procedures."""
|
||||
_LOGGER.debug("Ending Alert: %s", self._name)
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
yield from self._notify_done_message()
|
||||
await self._notify_done_message()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _schedule_notify(self):
|
||||
async def _schedule_notify(self):
|
||||
"""Schedule a notification."""
|
||||
delay = self._delay[self._next_delay]
|
||||
next_msg = datetime.now() + delay
|
||||
@@ -240,8 +196,7 @@ class Alert(ToggleEntity):
|
||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _notify(self, *args):
|
||||
async def _notify(self, *args):
|
||||
"""Send the alert notification."""
|
||||
if not self._firing:
|
||||
return
|
||||
@@ -250,36 +205,32 @@ class Alert(ToggleEntity):
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
yield from self._schedule_notify()
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name})
|
||||
await self._schedule_notify()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _notify_done_message(self, *args):
|
||||
async def _notify_done_message(self, *args):
|
||||
"""Send notification of complete alert."""
|
||||
_LOGGER.info("Alerting: %s", self._done_message)
|
||||
self._send_done_message = False
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._done_message})
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_toggle(self, **kwargs):
|
||||
async def async_toggle(self, **kwargs):
|
||||
"""Async toggle alert."""
|
||||
if self._ack:
|
||||
return self.async_turn_on()
|
||||
return self.async_turn_off()
|
||||
return await self.async_turn_on()
|
||||
return await self.async_turn_off()
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Alexa skill service end point.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -53,8 +52,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
config = config.get(DOMAIN, {})
|
||||
flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS)
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Alexa skill service end point.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
|
||||
@@ -59,16 +58,15 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
message = yield from request.json()
|
||||
message = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa request: %s", message)
|
||||
|
||||
try:
|
||||
response = yield from async_handle_message(hass, message)
|
||||
response = await async_handle_message(hass, message)
|
||||
return b'' if response is None else self.json(response)
|
||||
except UnknownRequest as err:
|
||||
_LOGGER.warning(str(err))
|
||||
@@ -101,8 +99,7 @@ def intent_error_response(hass, message, error):
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
async def async_handle_message(hass, message):
|
||||
"""Handle an Alexa intent.
|
||||
|
||||
Raises:
|
||||
@@ -120,20 +117,18 @@ def async_handle_message(hass, message):
|
||||
if not handler:
|
||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
||||
|
||||
return (yield from handler(hass, message))
|
||||
return await handler(hass, message)
|
||||
|
||||
|
||||
@HANDLERS.register('SessionEndedRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_session_end(hass, message):
|
||||
async def async_handle_session_end(hass, message):
|
||||
"""Handle a session end request."""
|
||||
return None
|
||||
|
||||
|
||||
@HANDLERS.register('IntentRequest')
|
||||
@HANDLERS.register('LaunchRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_intent(hass, message):
|
||||
async def async_handle_intent(hass, message):
|
||||
"""Handle an intent request.
|
||||
|
||||
Raises:
|
||||
@@ -153,7 +148,7 @@ def async_handle_intent(hass, message):
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
|
||||
intent_response = yield from intent.async_handle(
|
||||
intent_response = await intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
@@ -695,8 +694,7 @@ class SmartHomeView(http.HomeAssistantView):
|
||||
"""Initialize."""
|
||||
self.smart_home_config = smart_home_config
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Handle Alexa Smart Home requests.
|
||||
|
||||
The Smart Home API requires the endpoint to be implemented in AWS
|
||||
@@ -704,11 +702,11 @@ class SmartHomeView(http.HomeAssistantView):
|
||||
the response.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
message = yield from request.json()
|
||||
message = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
|
||||
|
||||
response = yield from async_handle_message(
|
||||
response = await async_handle_message(
|
||||
hass, self.smart_home_config, message)
|
||||
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
||||
return b'' if response is None else self.json(response)
|
||||
@@ -1529,3 +1527,8 @@ async def async_api_reportstate(hass, config, request, context, entity):
|
||||
name='StateReport',
|
||||
context={'properties': properties}
|
||||
)
|
||||
|
||||
|
||||
def turned_off_response(message):
|
||||
"""Return a device turned off response."""
|
||||
return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE')
|
||||
|
||||
@@ -149,16 +149,14 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the IP Webcam component."""
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_ipcamera(cam_config):
|
||||
async def async_setup_ipcamera(cam_config):
|
||||
"""Set up an IP camera."""
|
||||
host = cam_config[CONF_HOST]
|
||||
username = cam_config.get(CONF_USERNAME)
|
||||
@@ -188,16 +186,15 @@ def async_setup(hass, config):
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
async def async_update_data(now):
|
||||
"""Update data from IP camera in SCAN_INTERVAL."""
|
||||
yield from cam.update()
|
||||
await cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_data, utcnow() + interval)
|
||||
|
||||
yield from async_update_data(None)
|
||||
await async_update_data(None)
|
||||
|
||||
# Load platforms
|
||||
webcams[host] = cam
|
||||
@@ -242,7 +239,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
@@ -255,8 +252,7 @@ class AndroidIPCamEntity(Entity):
|
||||
self._host = host
|
||||
self._ipcam = ipcam
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_ipcam_update(host):
|
||||
|
||||
@@ -49,10 +49,9 @@ def setup(hass, config):
|
||||
|
||||
# It doesn't really matter why we're not able to get the status, just that
|
||||
# we can't.
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
DATA.update(no_throttle=True)
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -141,6 +141,8 @@ class APIEventStream(HomeAssistantView):
|
||||
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
"""View to handle Configuration requests."""
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from typing import Sequence, TypeVar, Union
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -78,14 +77,13 @@ def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def configuration_callback(callback_data):
|
||||
async def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
await atv.airplay.finish_authentication(pin)
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
@@ -109,11 +107,10 @@ def request_configuration(hass, config, atv, credentials):
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_for_apple_tvs(hass):
|
||||
async def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
@@ -133,14 +130,12 @@ def scan_for_apple_tvs(hass):
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
async def async_service_handler(service):
|
||||
"""Handle service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
@@ -159,17 +154,16 @@ def async_setup(hass, config):
|
||||
continue
|
||||
|
||||
atv = device.atv
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
credentials = await atv.airplay.generate_credentials()
|
||||
await atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
await atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
async def atv_discovered(service, info):
|
||||
"""Set up an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
await _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
@@ -180,7 +174,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
@@ -193,8 +187,7 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
async def _setup_atv(hass, atv_config):
|
||||
"""Set up an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
@@ -210,7 +203,7 @@ def _setup_atv(hass, atv_config):
|
||||
session = async_get_clientsession(hass)
|
||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
await atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
@@ -259,4 +252,4 @@ class AppleTVPowerManager:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_add_job(listener.async_update_ha_state())
|
||||
self.hass.async_create_task(listener.async_update_ha_state())
|
||||
|
||||
95
homeassistant/components/aqualogic.py
Normal file
95
homeassistant/components/aqualogic.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Support for AquaLogic component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/aqualogic/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ["aqualogic==1.0"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "aqualogic"
|
||||
UPDATE_TOPIC = DOMAIN + "_update"
|
||||
CONF_UNIT = "unit"
|
||||
RECONNECT_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up AquaLogic platform."""
|
||||
host = config[DOMAIN][CONF_HOST]
|
||||
port = config[DOMAIN][CONF_PORT]
|
||||
processor = AquaLogicProcessor(hass, host, port)
|
||||
hass.data[DOMAIN] = processor
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
processor.start_listen)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
processor.shutdown)
|
||||
_LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port)
|
||||
return True
|
||||
|
||||
|
||||
class AquaLogicProcessor(threading.Thread):
|
||||
"""AquaLogic event processor thread."""
|
||||
|
||||
def __init__(self, hass, host, port):
|
||||
"""Initialize the data object."""
|
||||
super().__init__(daemon=True)
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._shutdown = False
|
||||
self._panel = None
|
||||
|
||||
def start_listen(self, event):
|
||||
"""Start event-processing thread."""
|
||||
_LOGGER.debug("Event processing thread started")
|
||||
self.start()
|
||||
|
||||
def shutdown(self, event):
|
||||
"""Signal shutdown of processing event."""
|
||||
_LOGGER.debug("Event processing signaled exit")
|
||||
self._shutdown = True
|
||||
|
||||
def data_changed(self, panel):
|
||||
"""Aqualogic data changed callback."""
|
||||
self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC)
|
||||
|
||||
def run(self):
|
||||
"""Event thread."""
|
||||
from aqualogic.core import AquaLogic
|
||||
|
||||
while True:
|
||||
self._panel = AquaLogic()
|
||||
self._panel.connect(self._host, self._port)
|
||||
self._panel.process(self.data_changed)
|
||||
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
_LOGGER.error("Connection to %s:%d lost",
|
||||
self._host, self._port)
|
||||
time.sleep(RECONNECT_INTERVAL.seconds)
|
||||
|
||||
@property
|
||||
def panel(self):
|
||||
"""Retrieve the AquaLogic object."""
|
||||
return self._panel
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||
REQUIREMENTS = ['pyarlo==0.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,10 +61,12 @@ def setup(hass, config):
|
||||
arlo_base_station = next((
|
||||
station for station in arlo.base_stations), None)
|
||||
|
||||
if arlo_base_station is None:
|
||||
if arlo_base_station is not None:
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
elif not arlo.cameras:
|
||||
_LOGGER.error("No Arlo camera or base station available.")
|
||||
return False
|
||||
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
@@ -79,7 +81,7 @@ def setup(hass, config):
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
_LOGGER.debug("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
||||
@@ -13,22 +13,25 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
async_dispatcher_send, dispatcher_connect)
|
||||
|
||||
REQUIREMENTS = ['asterisk_mbox==0.4.0']
|
||||
REQUIREMENTS = ['asterisk_mbox==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'asterisk_mbox'
|
||||
|
||||
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
|
||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
||||
SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated'
|
||||
SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -41,9 +44,7 @@ def setup(hass, config):
|
||||
port = conf.get(CONF_PORT)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
||||
|
||||
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
|
||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -51,31 +52,71 @@ def setup(hass, config):
|
||||
class AsteriskData:
|
||||
"""Store Asterisk mailbox data."""
|
||||
|
||||
def __init__(self, hass, host, port, password):
|
||||
def __init__(self, hass, host, port, password, config):
|
||||
"""Init the Asterisk data object."""
|
||||
from asterisk_mbox import Client as asteriskClient
|
||||
|
||||
self.hass = hass
|
||||
self.client = asteriskClient(host, port, password, self.handle_data)
|
||||
self.messages = []
|
||||
self.config = config
|
||||
self.messages = None
|
||||
self.cdr = None
|
||||
|
||||
async_dispatcher_connect(
|
||||
dispatcher_connect(
|
||||
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
|
||||
dispatcher_connect(
|
||||
self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
|
||||
dispatcher_connect(
|
||||
self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
|
||||
# Only connect after signal connection to ensure we don't miss any
|
||||
self.client = asteriskClient(host, port, password, self.handle_data)
|
||||
|
||||
@callback
|
||||
def _discover_platform(self, component):
|
||||
_LOGGER.debug("Adding mailbox %s", component)
|
||||
self.hass.async_create_task(discovery.async_load_platform(
|
||||
self.hass, "mailbox", component, {}, self.config))
|
||||
|
||||
@callback
|
||||
def handle_data(self, command, msg):
|
||||
"""Handle changes to the mailbox."""
|
||||
from asterisk_mbox.commands import CMD_MESSAGE_LIST
|
||||
from asterisk_mbox.commands import (CMD_MESSAGE_LIST,
|
||||
CMD_MESSAGE_CDR_AVAILABLE,
|
||||
CMD_MESSAGE_CDR)
|
||||
|
||||
if command == CMD_MESSAGE_LIST:
|
||||
_LOGGER.debug("AsteriskVM sent updated message list")
|
||||
_LOGGER.debug("AsteriskVM sent updated message list: Len %d",
|
||||
len(msg))
|
||||
old_messages = self.messages
|
||||
self.messages = sorted(
|
||||
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
||||
async_dispatcher_send(
|
||||
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
||||
if not isinstance(old_messages, list):
|
||||
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
|
||||
DOMAIN)
|
||||
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
|
||||
self.messages)
|
||||
elif command == CMD_MESSAGE_CDR:
|
||||
_LOGGER.debug("AsteriskVM sent updated CDR list: Len %d",
|
||||
len(msg.get('entries', [])))
|
||||
self.cdr = msg['entries']
|
||||
async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
|
||||
elif command == CMD_MESSAGE_CDR_AVAILABLE:
|
||||
if not isinstance(self.cdr, list):
|
||||
_LOGGER.debug("AsteriskVM adding CDR platform")
|
||||
self.cdr = []
|
||||
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
|
||||
"asterisk_cdr")
|
||||
async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
|
||||
else:
|
||||
_LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d",
|
||||
command, len(msg))
|
||||
|
||||
@callback
|
||||
def _request_messages(self):
|
||||
"""Handle changes to the mailbox."""
|
||||
_LOGGER.debug("Requesting message list")
|
||||
self.client.messages()
|
||||
|
||||
@callback
|
||||
def _request_cdr(self):
|
||||
"""Handle changes to the CDR."""
|
||||
_LOGGER.debug("Requesting CDR list")
|
||||
self.client.get_cdr()
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for August devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/august/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -124,6 +123,7 @@ def setup_august(hass, config, api, authenticator):
|
||||
|
||||
return True
|
||||
if state == AuthenticationState.BAD_PASSWORD:
|
||||
_LOGGER.error("Invalid password provided")
|
||||
return False
|
||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
@@ -165,6 +165,7 @@ class AugustData:
|
||||
self._doorbell_detail_by_id = {}
|
||||
self._lock_status_by_id = {}
|
||||
self._lock_detail_by_id = {}
|
||||
self._door_state_by_id = {}
|
||||
self._activities_by_id = {}
|
||||
|
||||
@property
|
||||
@@ -184,6 +185,7 @@ class AugustData:
|
||||
|
||||
def get_device_activities(self, device_id, *activity_types):
|
||||
"""Return a list of activities."""
|
||||
_LOGGER.debug("Getting device activities")
|
||||
self._update_device_activities()
|
||||
|
||||
activities = self._activities_by_id.get(device_id, [])
|
||||
@@ -199,6 +201,7 @@ class AugustData:
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
_LOGGER.debug("Updating device activities")
|
||||
for house_id in self.house_ids:
|
||||
activities = self._api.get_house_activities(self._access_token,
|
||||
house_id,
|
||||
@@ -218,14 +221,21 @@ class AugustData:
|
||||
def _update_doorbells(self):
|
||||
detail_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving doorbell details")
|
||||
for doorbell in self._doorbells:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
doorbell.device_name)
|
||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving doorbell details")
|
||||
self._doorbell_detail_by_id = detail_by_id
|
||||
|
||||
def get_lock_status(self, lock_id):
|
||||
"""Return lock status."""
|
||||
"""Return status if the door is locked or unlocked.
|
||||
|
||||
This is status for the lock itself.
|
||||
"""
|
||||
self._update_locks()
|
||||
return self._lock_status_by_id.get(lock_id)
|
||||
|
||||
@@ -234,17 +244,43 @@ class AugustData:
|
||||
self._update_locks()
|
||||
return self._lock_detail_by_id.get(lock_id)
|
||||
|
||||
def get_door_state(self, lock_id):
|
||||
"""Return status if the door is open or closed.
|
||||
|
||||
This is the status from the door sensor.
|
||||
"""
|
||||
self._update_doors()
|
||||
return self._door_state_by_id.get(lock_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_doors(self):
|
||||
state_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving door status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
lock.device_name)
|
||||
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving door status")
|
||||
self._door_state_by_id = state_by_id
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_locks(self):
|
||||
status_by_id = {}
|
||||
detail_by_id = {}
|
||||
|
||||
_LOGGER.debug("Start retrieving locks status")
|
||||
for lock in self._locks:
|
||||
_LOGGER.debug("Updating status for %s",
|
||||
lock.device_name)
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
_LOGGER.debug("Completed retrieving locks status")
|
||||
self._lock_status_by_id = status_by_id
|
||||
self._lock_detail_by_id = detail_by_id
|
||||
|
||||
|
||||
7
homeassistant/components/auth/.translations/ar.json
Normal file
7
homeassistant/components/auth/.translations/ar.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,27 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Seleccioneu un dels serveis de notificaci\u00f3:",
|
||||
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
|
||||
},
|
||||
"setup": {
|
||||
"description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
|
||||
"title": "Verifiqueu la configuraci\u00f3"
|
||||
}
|
||||
},
|
||||
"title": "Contrasenya d'un sol \u00fas del servei de notificacions"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
||||
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
35
homeassistant/components/auth/.translations/de.json
Normal file
35
homeassistant/components/auth/.translations/de.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
|
||||
"title": "Einmal Passwort f\u00fcr Notify einrichten"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:",
|
||||
"title": "\u00dcberpr\u00fcfe das Setup"
|
||||
}
|
||||
},
|
||||
"title": "Benachrichtig f\u00fcr One-Time Password"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.",
|
||||
"title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,24 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "No notification services available."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Please select one of the notification services:",
|
||||
"title": "Set up one-time password delivered by notify component"
|
||||
},
|
||||
"setup": {
|
||||
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
|
||||
"title": "Verify setup"
|
||||
}
|
||||
},
|
||||
"title": "Notify One-Time Password"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
|
||||
|
||||
12
homeassistant/components/auth/.translations/es-419.json
Normal file
12
homeassistant/components/auth/.translations/es-419.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/fr.json
Normal file
35
homeassistant/components/auth/.translations/fr.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Aucun service de notification disponible."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Veuillez s\u00e9lectionner l'un des services de notification:",
|
||||
"title": "Configurer un mot de passe \u00e0 usage unique d\u00e9livr\u00e9 par le composant notify"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :",
|
||||
"title": "V\u00e9rifier la configuration"
|
||||
}
|
||||
},
|
||||
"title": "Notifier un mot de passe unique"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Pour activer l'authentification \u00e0 deux facteurs \u00e0 l'aide de mots de passe \u00e0 utilisation unique bas\u00e9s sur l'heure, num\u00e9risez le code QR avec votre application d'authentification. Si vous n'en avez pas, nous vous recommandons d'utiliser [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Apr\u00e8s avoir num\u00e9ris\u00e9 le code, entrez le code \u00e0 six chiffres de votre application pour v\u00e9rifier la configuration. Si vous rencontrez des probl\u00e8mes lors de l\u2019analyse du code QR, effectuez une configuration manuelle avec le code ** ` {code} ` **.",
|
||||
"title": "Configurer une authentification \u00e0 deux facteurs \u00e0 l'aide de TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP (Mot de passe \u00e0 utilisation unique bas\u00e9 sur le temps)"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/he.json
Normal file
35
homeassistant/components/auth/.translations/he.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify",
|
||||
"title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify"
|
||||
},
|
||||
"setup": {
|
||||
"description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:",
|
||||
"title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4"
|
||||
}
|
||||
},
|
||||
"title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.",
|
||||
"title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
homeassistant/components/auth/.translations/hu.json
Normal file
32
homeassistant/components/auth/.translations/hu.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Nincs el\u00e9rhet\u0151 \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:"
|
||||
},
|
||||
"setup": {
|
||||
"title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se"
|
||||
}
|
||||
}
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.",
|
||||
"title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/id.json
Normal file
16
homeassistant/components/auth/.translations/id.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.",
|
||||
"title": "Siapkan otentikasi dua faktor menggunakan TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
homeassistant/components/auth/.translations/it.json
Normal file
13
homeassistant/components/auth/.translations/it.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.",
|
||||
"title": "Imposta l'autenticazione a due fattori usando TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uacc4\uac00 \uc815\ud655\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
|
||||
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub098 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
|
||||
"title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
|
||||
"description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
|
||||
"title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815"
|
||||
},
|
||||
"setup": {
|
||||
"description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:",
|
||||
"title": "\uc124\uc815 \ud655\uc778"
|
||||
}
|
||||
},
|
||||
"title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
|
||||
"title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
|
||||
}
|
||||
},
|
||||
"title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)"
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:",
|
||||
"title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:",
|
||||
"title": "Astellungen iwwerpr\u00e9iwen"
|
||||
}
|
||||
},
|
||||
"title": "Eemolegt Passwuert Notifikatioun"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."
|
||||
|
||||
35
homeassistant/components/auth/.translations/nl.json
Normal file
35
homeassistant/components/auth/.translations/nl.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Geen meldingsservices beschikbaar."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Ongeldige code, probeer opnieuw."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Selecteer een van de meldingsdiensten:",
|
||||
"title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Een \u00e9\u00e9nmalig wachtwoord is verzonden via **notify. {notify_service}**. Voer het hieronder in:",
|
||||
"title": "Controleer de instellingen"
|
||||
}
|
||||
},
|
||||
"title": "Eenmalig wachtwoord melden"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Voor het activeren van twee-factor-authenticatie via tijdgebonden eenmalige wachtwoorden: scan de QR code met uw authenticatie-app. Als u nog geen app heeft, adviseren we [Google Authenticator (https://support.google.com/accounts/answer/1066447) of [Authy](https://authy.com/).\n\n{qr_code}\n\nNa het scannen van de code voert u de zescijferige code uit uw app in om de instelling te controleren. Als u problemen heeft met het scannen van de QR-code, voert u een handmatige configuratie uit met code **`{code}`**.",
|
||||
"title": "Configureer twee-factor-authenticatie via TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/nn.json
Normal file
16
homeassistant/components/auth/.translations/nn.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Dersom du heile tida f\u00e5r denne feilen, m\u00e5 du s\u00f8rge for at klokka p\u00e5 Home Assistant-systemet ditt er n\u00f8yaktig."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte eingangspassord, skann QR-koden med autentiseringsappen din. Dersom du ikkje har ein, vil vi r\u00e5de deg til \u00e5 bruke anten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har skanna koda, skriv du inn den sekssifra koda fr\u00e5 appen din for \u00e5 stadfeste oppsettet. Dersom du har problemer med \u00e5 skanne QR-koda, gjer du eit manuelt oppsett med kode ** ` {code} ` **.",
|
||||
"title": "Konfigurer to-faktor-autentisering ved hjelp av TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/no.json
Normal file
35
homeassistant/components/auth/.translations/no.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Ingen varslingstjenester er tilgjengelig."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Vennligst velg en av varslingstjenestene:",
|
||||
"title": "Sett opp engangspassord levert av varsel komponent"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:",
|
||||
"title": "Bekreft oppsettet"
|
||||
}
|
||||
},
|
||||
"title": "Varsle engangspassord"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/). \n\n {qr_code} \n \nEtter at du har skannet koden, skriver du inn den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du taste inn f\u00f8lgende kode manuelt: **`{code}`**.",
|
||||
"title": "Konfigurer tofaktorautentisering ved hjelp av TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/pl.json
Normal file
35
homeassistant/components/auth/.translations/pl.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:",
|
||||
"title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:",
|
||||
"title": "Sprawd\u017a konfiguracj\u0119"
|
||||
}
|
||||
},
|
||||
"title": "Powiadomienie z has\u0142em jednorazowym"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Aby aktywowa\u0107 uwierzytelnianie dwusk\u0142adnikowe przy u\u017cyciu jednorazowych hase\u0142 opartych na czasie, zeskanuj kod QR za pomoc\u0105 aplikacji uwierzytelniaj\u0105cej. Je\u015bli jej nie masz, polecamy [Google Authenticator](https://support.google.com/accounts/answer/1066447) lub [Authy](https://authy.com/).\n\n{qr_code} \n \nPo zeskanowaniu kodu wprowad\u017a sze\u015bciocyfrowy kod z aplikacji, aby zweryfikowa\u0107 konfiguracj\u0119. Je\u015bli masz problemy z zeskanowaniem kodu QR, wykonaj r\u0119czn\u0105 konfiguracj\u0119 z kodem **`{code}`**.",
|
||||
"title": "Skonfiguruj uwierzytelnianie dwusk\u0142adnikowe za pomoc\u0105 hase\u0142 jednorazowych opartych na czasie"
|
||||
}
|
||||
},
|
||||
"title": "Has\u0142a jednorazowe oparte na czasie"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/auth/.translations/pt-BR.json
Normal file
15
homeassistant/components/auth/.translations/pt-BR.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/pt.json
Normal file
35
homeassistant/components/auth/.translations/pt.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:",
|
||||
"title": "Configurar uma palavra passe entregue pela componente de notifica\u00e7\u00e3o"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Foi enviada uma palavra passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:",
|
||||
"title": "Verificar a configura\u00e7\u00e3o"
|
||||
}
|
||||
},
|
||||
"title": "Notificar palavra passe de uso \u00fanico"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando passwords unicas temporais (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{c\u00f3digo}`**.",
|
||||
"title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
homeassistant/components/auth/.translations/ro.json
Normal file
34
homeassistant/components/auth/.translations/ro.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Nu sunt disponibile servicii de notificare."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Cod invalid, va rugam incercati din nou."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
|
||||
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
|
||||
},
|
||||
"setup": {
|
||||
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
|
||||
"title": "Verifica\u021bi configurarea"
|
||||
}
|
||||
},
|
||||
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,31 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439"
|
||||
},
|
||||
"setup": {
|
||||
"description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:",
|
||||
"title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443"
|
||||
}
|
||||
},
|
||||
"title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator] (https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043a\u043e\u0434\u043e\u043c ** ` {code} ` **.",
|
||||
"description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP"
|
||||
}
|
||||
},
|
||||
|
||||
35
homeassistant/components/auth/.translations/sl.json
Normal file
35
homeassistant/components/auth/.translations/sl.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Neveljavna koda, poskusite znova."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
|
||||
"title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
|
||||
"title": "Preverite nastavitev"
|
||||
}
|
||||
},
|
||||
"title": "Obvesti Enkratno Geslo"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u010ce \u017eelite aktivirati preverjanje pristnosti dveh faktorjev z enkratnimi gesli, ki temeljijo na \u010dasu, skenirajte kodo QR s svojo aplikacijo za preverjanje pristnosti. \u010ce je nimate, priporo\u010damo bodisi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ali [Authy] (https://authy.com/). \n\n {qr_code} \n \n Po skeniranju kode vnesite \u0161estmestno kodo iz aplikacije, da preverite nastavitev. \u010ce imate te\u017eave pri skeniranju kode QR, naredite ro\u010dno nastavitev s kodo ** ` {code} ` **.",
|
||||
"title": "Nastavite dvofaktorsko avtentifikacijo s pomo\u010djo TOTP-ja"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/sv.json
Normal file
35
homeassistant/components/auth/.translations/sv.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Var god v\u00e4lj en av notifieringstj\u00e4nsterna:",
|
||||
"title": "Konfigurera ett eng\u00e5ngsl\u00f6senord levererat genom notifieringskomponenten"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:",
|
||||
"title": "Verifiera inst\u00e4llningen"
|
||||
}
|
||||
},
|
||||
"title": "Meddela eng\u00e5ngsl\u00f6senord"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "F\u00f6r att aktivera tv\u00e5faktorsautentisering som anv\u00e4nder tidsbaserade eng\u00e5ngsl\u00f6senord, skanna QR-koden med din autentiseringsapp. Om du inte har en, rekommenderar vi antingen [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n{qr_code} \n\nN\u00e4r du har skannat koden anger du den sexsiffriga koden fr\u00e5n din app f\u00f6r att verifiera inst\u00e4llningen. Om du har problem med att skanna QR-koden, g\u00f6r en manuell inst\u00e4llning med kod ** ` {code} ` **.",
|
||||
"title": "St\u00e4ll in tv\u00e5faktorsautentisering med TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
homeassistant/components/auth/.translations/uk.json
Normal file
9
homeassistant/components/auth/.translations/uk.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"error": {
|
||||
"invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,24 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002"
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a",
|
||||
"title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801"
|
||||
},
|
||||
"setup": {
|
||||
"description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a",
|
||||
"title": "\u9a8c\u8bc1\u8bbe\u7f6e"
|
||||
}
|
||||
},
|
||||
"title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002"
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a",
|
||||
"title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6"
|
||||
},
|
||||
"setup": {
|
||||
"description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a",
|
||||
"title": "\u9a57\u8b49\u8a2d\u5b9a"
|
||||
}
|
||||
},
|
||||
"title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc"
|
||||
},
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
|
||||
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
|
||||
"title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ be in JSON as it's more readable.
|
||||
Exchange the authorization code retrieved from the login flow for tokens.
|
||||
|
||||
{
|
||||
"client_id": "https://hassbian.local:8123/",
|
||||
"grant_type": "authorization_code",
|
||||
"code": "411ee2f916e648d691e937ae9344681e"
|
||||
}
|
||||
@@ -32,6 +33,7 @@ token.
|
||||
Request a new access token using a refresh token.
|
||||
|
||||
{
|
||||
"client_id": "https://hassbian.local:8123/",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": "IJKLMNOPQRST"
|
||||
}
|
||||
@@ -55,6 +57,66 @@ ever been granted by that refresh token. Response code will ALWAYS be 200.
|
||||
"action": "revoke"
|
||||
}
|
||||
|
||||
# Websocket API
|
||||
|
||||
## Get current user
|
||||
|
||||
Send websocket command `auth/current_user` will return current user of the
|
||||
active websocket connection.
|
||||
|
||||
{
|
||||
"id": 10,
|
||||
"type": "auth/current_user",
|
||||
}
|
||||
|
||||
The result payload likes
|
||||
|
||||
{
|
||||
"id": 10,
|
||||
"type": "result",
|
||||
"success": true,
|
||||
"result": {
|
||||
"id": "USER_ID",
|
||||
"name": "John Doe",
|
||||
"is_owner': true,
|
||||
"credentials": [
|
||||
{
|
||||
"auth_provider_type": "homeassistant",
|
||||
"auth_provider_id": null
|
||||
}
|
||||
],
|
||||
"mfa_modules": [
|
||||
{
|
||||
"id": "totp",
|
||||
"name": "TOTP",
|
||||
"enabled": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
## Create a long-lived access token
|
||||
|
||||
Send websocket command `auth/long_lived_access_token` will create
|
||||
a long-lived access token for current user. Access token will not be saved in
|
||||
Home Assistant. User need to record the token in secure place.
|
||||
|
||||
{
|
||||
"id": 11,
|
||||
"type": "auth/long_lived_access_token",
|
||||
"client_name": "GPS Logger",
|
||||
"lifespan": 365
|
||||
}
|
||||
|
||||
Result will be a long-lived access token:
|
||||
|
||||
{
|
||||
"id": 11,
|
||||
"type": "result",
|
||||
"success": true,
|
||||
"result": "ABCDEFGH"
|
||||
}
|
||||
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
@@ -63,8 +125,10 @@ from datetime import timedelta
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User, Credentials
|
||||
from homeassistant.auth.models import User, Credentials, \
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import KEY_REAL_IP
|
||||
from homeassistant.components.http.ban import log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
@@ -83,6 +147,28 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
||||
})
|
||||
|
||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
vol.Required('lifespan'): int, # days
|
||||
vol.Required('client_name'): str,
|
||||
vol.Optional('client_icon'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
|
||||
SCHEMA_WS_REFRESH_TOKENS = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
|
||||
})
|
||||
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
vol.Required('refresh_token_id'): str,
|
||||
})
|
||||
|
||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||
RESULT_TYPE_USER = 'user'
|
||||
|
||||
@@ -100,6 +186,21 @@ async def async_setup(hass, config):
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
websocket_create_long_lived_access_token,
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_REFRESH_TOKENS,
|
||||
websocket_refresh_tokens,
|
||||
SCHEMA_WS_REFRESH_TOKENS
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
websocket_delete_refresh_token,
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_result)
|
||||
await mfa_setup_flow.async_setup(hass)
|
||||
@@ -135,10 +236,12 @@ class TokenView(HomeAssistantView):
|
||||
return await self._async_handle_revoke_token(hass, data)
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(hass, data)
|
||||
return await self._async_handle_auth_code(
|
||||
hass, data, str(request[KEY_REAL_IP]))
|
||||
|
||||
if grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(hass, data)
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, data, str(request[KEY_REAL_IP]))
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
@@ -163,7 +266,7 @@ class TokenView(HomeAssistantView):
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
return web.Response(status=200)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, data):
|
||||
async def _async_handle_auth_code(self, hass, data, remote_addr):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||
@@ -199,7 +302,8 @@ class TokenView(HomeAssistantView):
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||
client_id)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token, remote_addr)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token,
|
||||
@@ -209,7 +313,7 @@ class TokenView(HomeAssistantView):
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
|
||||
async def _async_handle_refresh_token(self, hass, data):
|
||||
async def _async_handle_refresh_token(self, hass, data, remote_addr):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is not None and not indieauth.verify_client_id(client_id):
|
||||
@@ -237,7 +341,8 @@ class TokenView(HomeAssistantView):
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token, remote_addr)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token,
|
||||
@@ -327,7 +432,7 @@ def websocket_current_user(
|
||||
"""Get current user."""
|
||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||
|
||||
connection.send_message_outside(
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
@@ -343,3 +448,68 @@ def websocket_current_user(
|
||||
}))
|
||||
|
||||
hass.async_create_task(async_get_current_user(connection.user))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_create_long_lived_access_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Create or a long-lived access token."""
|
||||
async def async_create_long_lived_access_token(user):
|
||||
"""Create or a long-lived access token."""
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
user,
|
||||
client_name=msg['client_name'],
|
||||
client_icon=msg.get('client_icon'),
|
||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
||||
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], access_token))
|
||||
|
||||
hass.async_create_task(
|
||||
async_create_long_lived_access_token(connection.user))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_refresh_tokens(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Return metadata of users refresh tokens."""
|
||||
current_id = connection.refresh_token_id
|
||||
connection.send_message(websocket_api.result_message(msg['id'], [{
|
||||
'id': refresh.id,
|
||||
'client_id': refresh.client_id,
|
||||
'client_name': refresh.client_name,
|
||||
'client_icon': refresh.client_icon,
|
||||
'type': refresh.token_type,
|
||||
'created_at': refresh.created_at,
|
||||
'is_current': refresh.id == current_id,
|
||||
'last_used_at': refresh.last_used_at,
|
||||
'last_used_ip': refresh.last_used_ip,
|
||||
} for refresh in connection.user.refresh_tokens.values()]))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_delete_refresh_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Handle a delete refresh token request."""
|
||||
async def async_delete_refresh_token(user, refresh_token_id):
|
||||
"""Delete a refresh token."""
|
||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
||||
|
||||
if refresh_token is None:
|
||||
return websocket_api.error_message(
|
||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg['id'], {}))
|
||||
|
||||
hass.async_create_task(
|
||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
import asyncio
|
||||
from ipaddress import ip_address
|
||||
from html.parser import HTMLParser
|
||||
from ipaddress import ip_address, ip_network
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
ALLOWED_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
)
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
|
||||
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||
@@ -185,9 +174,7 @@ def _parse_client_id(client_id):
|
||||
# Not an ip address
|
||||
pass
|
||||
|
||||
if (address is None or
|
||||
address in ALLOWED_IPS or
|
||||
any(address in network for network in ALLOWED_NETWORKS)):
|
||||
if address is None or is_local(address):
|
||||
return parts
|
||||
|
||||
raise ValueError('Hostname should be a domain name or local IP address')
|
||||
|
||||
@@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in
|
||||
"version": 1
|
||||
}
|
||||
"""
|
||||
import aiohttp.web
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
return self.json_message(
|
||||
message='Onboarding not finished',
|
||||
status_code=400,
|
||||
message_code='onboarding_required'
|
||||
)
|
||||
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
} for provider in hass.auth.auth_providers])
|
||||
|
||||
|
||||
def _prepare_result_json(result):
|
||||
@@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
return web.Response(status=405)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
@@ -217,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||
# need manually log failed login attempts
|
||||
if result['errors'] is not None and \
|
||||
result['errors'].get('base') == 'invalid_auth':
|
||||
if (result.get('errors') is not None and
|
||||
result['errors'].get('base') in ['invalid_auth',
|
||||
'invalid_code']):
|
||||
await process_wrong_login(request)
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def websocket_setup_mfa(
|
||||
if flow_id is not None:
|
||||
result = await flow_manager.async_configure(
|
||||
flow_id, msg.get('user_input'))
|
||||
connection.send_message_outside(
|
||||
connection.send_message(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
return
|
||||
@@ -72,7 +72,7 @@ def websocket_setup_mfa(
|
||||
mfa_module_id = msg.get('mfa_module_id')
|
||||
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
||||
if mfa_module is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'no_module',
|
||||
'MFA module {} is not found'.format(mfa_module_id)))
|
||||
return
|
||||
@@ -80,7 +80,7 @@ def websocket_setup_mfa(
|
||||
result = await flow_manager.async_init(
|
||||
mfa_module_id, data={'user_id': connection.user.id})
|
||||
|
||||
connection.send_message_outside(
|
||||
connection.send_message(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
|
||||
@@ -99,13 +99,13 @@ def websocket_depose_mfa(
|
||||
await hass.auth.async_disable_user_mfa(
|
||||
connection.user, msg['mfa_module_id'])
|
||||
except ValueError as err:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'disable_failed',
|
||||
'Cannot disable MFA Module {}: {}'.format(
|
||||
mfa_module_id, err)))
|
||||
return
|
||||
|
||||
connection.send_message_outside(
|
||||
connection.send_message(
|
||||
websocket_api.result_message(
|
||||
msg['id'], 'done'))
|
||||
|
||||
|
||||
@@ -11,6 +11,25 @@
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"title": "Notify One-Time Password",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up one-time password delivered by notify component",
|
||||
"description": "Please select one of the notification services:"
|
||||
},
|
||||
"setup": {
|
||||
"title": "Verify setup",
|
||||
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"no_available_service": "No notification services available."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
@@ -115,70 +114,26 @@ def is_on(hass, entity_id):
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_on(hass, entity_id=None):
|
||||
"""Turn on specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_off(hass, entity_id=None):
|
||||
"""Turn off specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def toggle(hass, entity_id=None):
|
||||
"""Toggle specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def trigger(hass, entity_id=None):
|
||||
"""Trigger specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config.
|
||||
|
||||
Returns a coroutine object.
|
||||
"""
|
||||
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
await _async_process_config(hass, config, component)
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
async def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
service_call.data.get(ATTR_VARIABLES),
|
||||
skip_condition=True,
|
||||
context=service_call.context))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
async def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
tasks = []
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
@@ -186,10 +141,9 @@ def async_setup(hass, config):
|
||||
tasks.append(getattr(entity, method)())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
async def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
@@ -199,15 +153,14 @@ def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_on())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
async def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
conf = yield from component.async_prepare_reload()
|
||||
conf = await component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
yield from _async_process_config(hass, conf, component)
|
||||
await _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
@@ -272,15 +225,14 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._async_detach_triggers is not None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug("Automation %s initial state %s from config "
|
||||
"initial_state", self.entity_id, enable_automation)
|
||||
else:
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
@@ -298,54 +250,50 @@ class AutomationEntity(ToggleEntity):
|
||||
|
||||
# HomeAssistant is starting up
|
||||
if self.hass.state == CoreState.not_running:
|
||||
@asyncio.coroutine
|
||||
def async_enable_automation(event):
|
||||
async def async_enable_automation(event):
|
||||
"""Start automation on startup."""
|
||||
yield from self.async_enable()
|
||||
await self.async_enable()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_enable_automation)
|
||||
|
||||
# HomeAssistant is running
|
||||
else:
|
||||
yield from self.async_enable()
|
||||
await self.async_enable()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
yield from self.async_enable()
|
||||
await self.async_enable()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
async def async_trigger(self, variables, skip_condition=False,
|
||||
context=None):
|
||||
"""Trigger automation.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(self.entity_id, variables)
|
||||
self.async_set_context(context)
|
||||
await self._async_action(self.entity_id, variables, context)
|
||||
self._last_triggered = utcnow()
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_will_remove_from_hass(self):
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove listeners when removing automation from HASS."""
|
||||
yield from self.async_turn_off()
|
||||
await self.async_turn_off()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
async def async_enable(self):
|
||||
"""Enable this automation entity.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -353,9 +301,9 @@ class AutomationEntity(ToggleEntity):
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||
self._async_detach_triggers = await self._async_attach_triggers(
|
||||
self.async_trigger)
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -368,8 +316,7 @@ class AutomationEntity(ToggleEntity):
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_process_config(hass, config, component):
|
||||
async def _async_process_config(hass, config, component):
|
||||
"""Process config and add automations.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -411,20 +358,19 @@ def _async_process_config(hass, config, component):
|
||||
entities.append(entity)
|
||||
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
await component.async_add_entities(entities)
|
||||
|
||||
|
||||
def _async_get_action(hass, config, name):
|
||||
"""Return an action based on a configuration."""
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(entity_id, variables):
|
||||
async def action(entity_id, variables, context):
|
||||
"""Execute an action."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
yield from script_obj.async_run(variables)
|
||||
hass.components.logbook.async_log_entry(
|
||||
name, 'has been triggered', DOMAIN, entity_id)
|
||||
await script_obj.async_run(variables, context)
|
||||
|
||||
return action
|
||||
|
||||
@@ -448,8 +394,7 @@ def _async_process_if(hass, config, p_config):
|
||||
return if_action
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Set up the triggers.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -457,13 +402,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
removes = []
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
platform = await async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = yield from platform.async_trigger(hass, conf, action)
|
||||
remove = await platform.async_trigger(hass, conf, action)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer event listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#event-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data_schema = vol.Schema(
|
||||
@@ -45,11 +43,11 @@ def async_trigger(hass, config, action):
|
||||
# If event data doesn't match requested schema, skip event
|
||||
return
|
||||
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
}, context=event.context))
|
||||
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
||||
74
homeassistant/components/automation/geo_location.py
Normal file
74
homeassistant/components/automation/geo_location.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Offer geo location automation rules.
|
||||
|
||||
For more details about this automation trigger, please refer to the
|
||||
documentation at
|
||||
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import DOMAIN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
|
||||
from homeassistant.helpers import (
|
||||
condition, config_validation as cv)
|
||||
from homeassistant.helpers.config_validation import entity_domain
|
||||
|
||||
EVENT_ENTER = 'enter'
|
||||
EVENT_LEAVE = 'leave'
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'geo_location',
|
||||
vol.Required(CONF_SOURCE): cv.string,
|
||||
vol.Required(CONF_ZONE): entity_domain('zone'),
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
||||
})
|
||||
|
||||
|
||||
def source_match(state, source):
|
||||
"""Check if the state matches the provided source."""
|
||||
return state and state.attributes.get('source') == source
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
source = config.get(CONF_SOURCE).lower()
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
trigger_event = config.get(CONF_EVENT)
|
||||
|
||||
@callback
|
||||
def state_change_listener(event):
|
||||
"""Handle specific state changes."""
|
||||
# Skip if the event is not a geo_location entity.
|
||||
if not event.data.get('entity_id').startswith(DOMAIN):
|
||||
return
|
||||
# Skip if the event's source does not match the trigger's source.
|
||||
from_state = event.data.get('old_state')
|
||||
to_state = event.data.get('new_state')
|
||||
if not source_match(from_state, source) \
|
||||
and not source_match(to_state, source):
|
||||
return
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
from_match = condition.zone(hass, zone_state, from_state)
|
||||
to_match = condition.zone(hass, zone_state, to_state)
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if trigger_event == EVENT_ENTER and not from_match and to_match or \
|
||||
trigger_event == EVENT_LEAVE and from_match and not to_match:
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'geo_location',
|
||||
'source': source,
|
||||
'entity_id': event.data.get('entity_id'),
|
||||
'from_state': from_state,
|
||||
'to_state': to_state,
|
||||
'zone': zone_state,
|
||||
'event': trigger_event,
|
||||
},
|
||||
}, context=event.context))
|
||||
|
||||
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)
|
||||
@@ -4,7 +4,6 @@ Offer Home Assistant core automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#homeassistant-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -23,8 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
@@ -32,12 +30,12 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def hass_shutdown(event):
|
||||
"""Execute when Home Assistant is shutting down."""
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
}, context=event.context))
|
||||
|
||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
hass_shutdown)
|
||||
@@ -45,11 +43,11 @@ def async_trigger(hass, config, action):
|
||||
# Automation are enabled while hass is starting up, fire right away
|
||||
# Check state because a config reload shouldn't trigger it.
|
||||
if hass.state == CoreState.starting:
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
return lambda: None
|
||||
|
||||
@@ -4,7 +4,6 @@ Trigger an automation when a LiteJet switch is released.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation.litejet/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -33,8 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer MQTT listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
@@ -51,6 +49,6 @@ def async_trigger(hass, config, action):
|
||||
'trigger': data
|
||||
})
|
||||
|
||||
remove = yield from mqtt.async_subscribe(
|
||||
remove = await mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener)
|
||||
return remove
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer numeric state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
@@ -66,7 +64,7 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
@@ -75,7 +73,7 @@ def async_trigger(hass, config, action):
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
})
|
||||
}, context=to_s.context))
|
||||
|
||||
matching = check_numeric_state(entity, from_s, to_s)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
@@ -43,7 +41,7 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
@@ -51,7 +49,7 @@ def async_trigger(hass, config, action):
|
||||
'to_state': to_s,
|
||||
'for': time_delta,
|
||||
}
|
||||
})
|
||||
}, context=to_s.context))
|
||||
|
||||
# Ignore changes to state attributes if from/to is in use
|
||||
if (not match_all and from_s is not None and to_s is not None and
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer sun based automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer template automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#template-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -23,8 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
@@ -32,13 +30,13 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def template_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
}, context=to_s.context))
|
||||
|
||||
return async_track_template(hass, value_template, template_listener)
|
||||
|
||||
@@ -4,7 +4,6 @@ Offer time listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
|
||||
53
homeassistant/components/automation/webhook.py
Normal file
53
homeassistant/components/automation/webhook.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Offer webhook triggered automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#webhook-trigger
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from aiohttp import hdrs
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ('webhook',)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'webhook',
|
||||
vol.Required(CONF_WEBHOOK_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def _handle_webhook(action, hass, webhook_id, request):
|
||||
"""Handle incoming webhook."""
|
||||
result = {
|
||||
'platform': 'webhook',
|
||||
'webhook_id': webhook_id,
|
||||
}
|
||||
|
||||
if 'json' in request.headers.get(hdrs.CONTENT_TYPE, ''):
|
||||
result['json'] = await request.json()
|
||||
else:
|
||||
result['data'] = await request.post()
|
||||
|
||||
hass.async_run_job(action, {'trigger': result})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Trigger based on incoming webhooks."""
|
||||
webhook_id = config.get(CONF_WEBHOOK_ID)
|
||||
hass.components.webhook.async_register(
|
||||
webhook_id, partial(_handle_webhook, action))
|
||||
|
||||
@callback
|
||||
def unregister():
|
||||
"""Unregister webhook."""
|
||||
hass.components.webhook.async_unregister(webhook_id)
|
||||
|
||||
return unregister
|
||||
@@ -4,7 +4,6 @@ Offer zone automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
async def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
@@ -51,7 +49,7 @@ def async_trigger(hass, config, action):
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
hass.async_run_job(action, {
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': entity,
|
||||
@@ -60,7 +58,7 @@ def async_trigger(hass, config, action):
|
||||
'zone': zone_state,
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
}, context=to_s.context))
|
||||
|
||||
return async_track_state_change(hass, entity_id, zone_automation_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
|
||||
@@ -38,6 +38,7 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_INCLUDE):
|
||||
@@ -47,7 +48,7 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for ADS binary sensors.
|
||||
For more details about this platform, please refer to the documentation.
|
||||
https://home-assistant.io/components/binary_sensor.ads/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -50,8 +49,7 @@ class AdsBinarySensor(BinarySensorDevice):
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for AlarmDecoder zone states- represented as binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
@@ -64,8 +63,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self._relay_addr = relay_addr
|
||||
self._relay_chan = relay_chan
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
@@ -4,8 +4,6 @@ Support for IP Webcam binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
||||
@@ -13,9 +11,8 @@ from homeassistant.components.android_ip_webcam import (
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -51,8 +48,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
state, _ = self._ipcam.export_sensor(self._sensor)
|
||||
self._state = state == 1.0
|
||||
|
||||
@@ -4,16 +4,26 @@ Support for August binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.august/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.components.august import DATA_AUGUST
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['august']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
def _retrieve_door_state(data, lock):
|
||||
"""Get the latest state of the DoorSense sensor."""
|
||||
from august.lock import LockDoorStatus
|
||||
doorstate = data.get_door_state(lock.device_id)
|
||||
return doorstate == LockDoorStatus.OPEN
|
||||
|
||||
|
||||
def _retrieve_online_state(data, doorbell):
|
||||
"""Get the latest state of the sensor."""
|
||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||
@@ -46,7 +56,11 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
||||
|
||||
|
||||
# Sensor types: Name, device_class, state_provider
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_TYPES_DOOR = {
|
||||
'door_open': ['Open', 'door', _retrieve_door_state],
|
||||
}
|
||||
|
||||
SENSOR_TYPES_DOORBELL = {
|
||||
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
||||
@@ -58,14 +72,78 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
||||
from august.lock import LockDoorStatus
|
||||
for door in data.locks:
|
||||
for sensor_type in SENSOR_TYPES_DOOR:
|
||||
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
|
||||
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
|
||||
_LOGGER.debug(
|
||||
"Not adding sensor class %s for lock %s ",
|
||||
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
|
||||
)
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding sensor class %s for %s",
|
||||
SENSOR_TYPES_DOOR[sensor_type][1], door.device_name
|
||||
)
|
||||
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
|
||||
for sensor_type in SENSOR_TYPES_DOORBELL:
|
||||
_LOGGER.debug("Adding doorbell sensor class %s for %s",
|
||||
SENSOR_TYPES_DOORBELL[sensor_type][1],
|
||||
doorbell.device_name)
|
||||
devices.append(
|
||||
AugustDoorbellBinarySensor(data, sensor_type,
|
||||
doorbell)
|
||||
)
|
||||
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustBinarySensor(BinarySensorDevice):
|
||||
class AugustDoorBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an August Door binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, door):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._door = door
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of this sensor."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES_DOOR[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._door.device_name,
|
||||
SENSOR_TYPES_DOOR[self._sensor_type][0])
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._door)
|
||||
|
||||
from august.lock import LockDoorStatus
|
||||
self._available = self._state != LockDoorStatus.UNKNOWN
|
||||
|
||||
|
||||
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an August binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, doorbell):
|
||||
@@ -83,15 +161,15 @@ class AugustBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES[self._sensor_type][1]
|
||||
return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._doorbell.device_name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
SENSOR_TYPES_DOORBELL[self._sensor_type][0])
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES[self._sensor_type][2]
|
||||
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._doorbell)
|
||||
|
||||
@@ -4,7 +4,6 @@ Use Bayesian Inference to trigger a binary sensor.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bayesian/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -74,9 +73,8 @@ def update_probability(prior, prob_true, prob_false):
|
||||
return probability
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Bayesian Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
@@ -119,8 +117,7 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
'state': self._process_state
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added."""
|
||||
@callback
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
@@ -214,7 +211,6 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
ATTR_PROBABILITY_THRESHOLD: self._probability_threshold,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability >= self._probability_threshold)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user