forked from home-assistant/core
Compare commits
1003 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7efe274c1 | ||
|
|
91b7d56aa6 | ||
|
|
1895e03874 | ||
|
|
d80dce31da | ||
|
|
94f24e6d49 | ||
|
|
2e169320a4 | ||
|
|
dce6a9f882 | ||
|
|
5a802c1069 | ||
|
|
602b59aed8 | ||
|
|
6f8ac7f5c9 | ||
|
|
5910161202 | ||
|
|
afc70fda50 | ||
|
|
2aabdf29bb | ||
|
|
1e03f945b5 | ||
|
|
a91d894132 | ||
|
|
66d0fb7dbf | ||
|
|
3a8891d9ac | ||
|
|
04e0fd1d46 | ||
|
|
b6a799499a | ||
|
|
29c9081ca1 | ||
|
|
1f07909a14 | ||
|
|
21686c9263 | ||
|
|
edf2974979 | ||
|
|
ea75e3bfa8 | ||
|
|
a0d4595f78 | ||
|
|
e28170a0a6 | ||
|
|
dcf8aba150 | ||
|
|
121a59abe0 | ||
|
|
0c7b0bdb44 | ||
|
|
c4b2c2bfcf | ||
|
|
bc67115df3 | ||
|
|
af03390c4f | ||
|
|
a4773dc3e4 | ||
|
|
ad903f9917 | ||
|
|
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 | ||
|
|
e77344f029 | ||
|
|
60438067f8 | ||
|
|
9062de0704 | ||
|
|
64453638bb | ||
|
|
5f1282a4ab | ||
|
|
645c3a67d8 | ||
|
|
88f72a654a | ||
|
|
87df102772 | ||
|
|
25ee8e551c | ||
|
|
3db766e2ec | ||
|
|
1eac6408f5 | ||
|
|
99d48795b9 | ||
|
|
5681fa8f07 | ||
|
|
867d17b03d | ||
|
|
7751dd7535 | ||
|
|
16a885824d | ||
|
|
3934f7bf3a | ||
|
|
96cf6d59a3 | ||
|
|
d46a1a266d | ||
|
|
aaa1ebeed5 | ||
|
|
18ba50bc2d | ||
|
|
3df8840fee | ||
|
|
630b5df59a | ||
|
|
9db15aab92 | ||
|
|
f01e1ef0aa | ||
|
|
b5919ce92c | ||
|
|
f9b1fb5906 | ||
|
|
9238261e17 | ||
|
|
e8801ee22f | ||
|
|
8ec109d255 | ||
|
|
74c0429437 | ||
|
|
563588651c | ||
|
|
63614a477a | ||
|
|
2ea2bcab77 | ||
|
|
8d38016b0c | ||
|
|
d994d6bfad | ||
|
|
573f5de148 | ||
|
|
667f9c6fe4 | ||
|
|
f708292015 | ||
|
|
c50a7deb92 | ||
|
|
11fcffda4c | ||
|
|
f891d0f5be | ||
|
|
257b8b9b80 | ||
|
|
9a786e449b | ||
|
|
09dc4d663d | ||
|
|
12709ceaa3 | ||
|
|
67df162bcc | ||
|
|
a14980716d | ||
|
|
376d4e4fa0 | ||
|
|
e9cc359abe | ||
|
|
9b01972b41 | ||
|
|
3e65009ea9 | ||
|
|
a953601abd | ||
|
|
2744702f9b | ||
|
|
9c7d4381a1 | ||
|
|
5397c0d73a | ||
|
|
8ab31fe139 | ||
|
|
45649824ca | ||
|
|
914436f3d5 | ||
|
|
6f0c30ff84 | ||
|
|
943260fcd6 | ||
|
|
24aa580b63 | ||
|
|
8435d2f53d | ||
|
|
c51170ef6d | ||
|
|
9d491f5322 | ||
|
|
adb5579690 | ||
|
|
94662620e2 | ||
|
|
f1e378bff8 | ||
|
|
2e9db1f5c4 | ||
|
|
dec2d8d5b0 | ||
|
|
a439690bd7 | ||
|
|
5d7a2f92df | ||
|
|
8413101148 | ||
|
|
8fb66c351e | ||
|
|
16ad9c2ae6 | ||
|
|
4da719f43c | ||
|
|
2ad938ed44 | ||
|
|
969b15a297 | ||
|
|
c8449d8f8a | ||
|
|
6992a6fe6d | ||
|
|
2ece671bfd | ||
|
|
c13e5fcb92 | ||
|
|
bacecb4249 | ||
|
|
47755fb1e9 | ||
|
|
69d104bcb6 | ||
|
|
3783d1ce90 | ||
|
|
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]]
|
||||
@@ -24,7 +29,11 @@ async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
|
||||
"""Initialize an auth manager from config."""
|
||||
"""Initialize an auth manager from config.
|
||||
|
||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||
mfa modules exist in configs.
|
||||
"""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
@@ -35,17 +44,7 @@ async def auth_manager_from_config(
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict() # type: _ProviderDict
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
|
||||
if module_configs:
|
||||
@@ -57,15 +56,6 @@ async def auth_manager_from_config(
|
||||
# So returned auth modules are in same order as config
|
||||
module_hash = OrderedDict() # type: _MfaModuleDict
|
||||
for module in modules:
|
||||
if module is None:
|
||||
continue
|
||||
|
||||
if module.id in module_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate multi-factor module: %s. Please add unique '
|
||||
'IDs if you want to have the same module twice.', module.id)
|
||||
continue
|
||||
|
||||
module_hash[module.id] = module
|
||||
|
||||
manager = AuthManager(hass, store, provider_hash, module_hash)
|
||||
@@ -139,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:
|
||||
@@ -175,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."""
|
||||
@@ -198,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)
|
||||
@@ -249,16 +264,20 @@ class AuthManager:
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> List[str]:
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
|
||||
"""List enabled mfa modules for user."""
|
||||
module_ids = []
|
||||
modules = OrderedDict() # type: Dict[str, str]
|
||||
for module_id, module in self._mfa_modules.items():
|
||||
if await module.async_is_user_setup(user.id):
|
||||
module_ids.append(module_id)
|
||||
return module_ids
|
||||
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:
|
||||
@@ -269,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]:
|
||||
@@ -292,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
|
||||
@@ -11,6 +10,7 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import requirements, data_entry_flow
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULES = Registry()
|
||||
@@ -22,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__)
|
||||
@@ -33,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."""
|
||||
@@ -83,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
|
||||
@@ -127,34 +126,32 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||
|
||||
async def auth_mfa_module_from_config(
|
||||
hass: HomeAssistant, config: Dict[str, Any]) \
|
||||
-> Optional[MultiFactorAuthModule]:
|
||||
-> MultiFactorAuthModule:
|
||||
"""Initialize an auth module from a config."""
|
||||
module_name = config[CONF_TYPE]
|
||||
module = await _load_mfa_module(hass, module_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
|
||||
module_name, humanize_error(config, err))
|
||||
return None
|
||||
raise
|
||||
|
||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||
|
||||
|
||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
-> Optional[types.ModuleType]:
|
||||
-> types.ModuleType:
|
||||
"""Load an mfa auth module."""
|
||||
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find %s', module_path)
|
||||
return None
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
|
||||
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
|
||||
module_name, err))
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
@@ -170,7 +167,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
hass, module_path, module.REQUIREMENTS) # type: ignore
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of mfa module {}'.format(
|
||||
module_name))
|
||||
|
||||
processed.add(module_name)
|
||||
return module
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
214
homeassistant/auth/mfa_modules/totp.py
Normal file
214
homeassistant/auth/mfa_modules/totp.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, Tuple # noqa: F401
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_module.totp'
|
||||
STORAGE_USERS = 'users'
|
||||
STORAGE_USER_ID = 'user_id'
|
||||
STORAGE_OTA_SECRET = 'ota_secret'
|
||||
|
||||
INPUT_FIELD_CODE = 'code'
|
||||
|
||||
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
import pyqrcode
|
||||
|
||||
qr_code = pyqrcode.create(data)
|
||||
|
||||
with BytesIO() as buffer:
|
||||
qr_code.svg(file=buffer, scale=4)
|
||||
return '{}'.format(
|
||||
buffer.getvalue().decode("ascii").replace('\n', '')
|
||||
.replace('<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
|
||||
)
|
||||
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
username, issuer_name="Home Assistant")
|
||||
image = _generate_qr_code(url)
|
||||
return ota_secret, url, image
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('totp')
|
||||
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, private=True)
|
||||
|
||||
@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._users = data.get(STORAGE_USERS, {})
|
||||
|
||||
async def _async_save(self) -> None:
|
||||
"""Save data."""
|
||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||
|
||||
def _add_ota_secret(self, user_id: str,
|
||||
secret: Optional[str] = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = secret or pyotp.random_base32() # type: str
|
||||
|
||||
self._users[user_id] = ota_secret # type: ignore
|
||||
return ota_secret
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
"""
|
||||
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
||||
return TotpSetupFlow(self, self.input_schema, user)
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||
"""Set up auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._add_ota_secret, user_id, setup_data.get('secret'))
|
||||
|
||||
await self._async_save()
|
||||
return result
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
if self._users.pop(user_id, None): # type: ignore
|
||||
await self._async_save()
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validate(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
# user_input has been validate in caller
|
||||
# set INPUT_FIELD_CODE as vol.Required is not user friendly
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = self._users.get(user_id) # type: ignore
|
||||
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, valid_window=1)
|
||||
return False
|
||||
|
||||
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
|
||||
|
||||
|
||||
class TotpSetupFlow(SetupFlow):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: TotpAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user: User) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user.id)
|
||||
# to fix typing complaint
|
||||
self._auth_module = auth_module # type: TotpAuthModule
|
||||
self._user = user
|
||||
self._ota_secret = None # type: Optional[str]
|
||||
self._url = None # type Optional[str]
|
||||
self._image = None # type Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp
|
||||
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
|
||||
if verified:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, {'secret': self._ota_secret})
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
)
|
||||
|
||||
errors['base'] = 'invalid_code'
|
||||
|
||||
else:
|
||||
hass = self._auth_module.hass
|
||||
self._ota_secret, self._url, self._image = \
|
||||
await hass.async_add_executor_job( # type: ignore
|
||||
_generate_secret_and_qr_code, str(self._user.name))
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=self._setup_schema,
|
||||
description_placeholders={
|
||||
'code': self._ota_secret,
|
||||
'url': self._url,
|
||||
'qr_code': self._image
|
||||
},
|
||||
errors=errors
|
||||
)
|
||||
@@ -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
|
||||
@@ -10,12 +10,13 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
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'
|
||||
@@ -110,33 +111,31 @@ class AuthProvider:
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> Optional[AuthProvider]:
|
||||
config: Dict[str, Any]) -> AuthProvider:
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
raise
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||
|
||||
|
||||
async def load_auth_provider_module(
|
||||
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]:
|
||||
hass: HomeAssistant, provider: str) -> types.ModuleType:
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth.providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
|
||||
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
|
||||
provider, err))
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
@@ -154,7 +153,9 @@ async def load_auth_provider_module(
|
||||
hass, 'auth provider {}'.format(provider), reqs)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of auth provider {}'.format(
|
||||
provider))
|
||||
|
||||
processed.add(provider)
|
||||
return module
|
||||
@@ -168,8 +169,9 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self._auth_provider = auth_provider
|
||||
self._auth_module_id = None # type: Optional[str]
|
||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
||||
self.available_mfa_modules = [] # type: List
|
||||
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(
|
||||
@@ -196,7 +198,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
errors['base'] = 'invalid_auth_module'
|
||||
|
||||
if len(self.available_mfa_modules) == 1:
|
||||
self._auth_module_id = self.available_mfa_modules[0]
|
||||
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
||||
return await self.async_step_mfa()
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -211,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(
|
||||
@@ -220,22 +224,39 @@ 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:
|
||||
errors['base'] = 'login_expired'
|
||||
else:
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
if not result:
|
||||
errors['base'] = 'invalid_auth'
|
||||
return self.async_abort(
|
||||
reason='login_expired'
|
||||
)
|
||||
|
||||
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, Optional[str]]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='mfa',
|
||||
data_schema=auth_module.input_schema,
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ import hashlib
|
||||
import hmac
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
|
||||
from ..models import Credentials, UserMeta
|
||||
from ..util import generate_secret
|
||||
|
||||
@@ -49,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:
|
||||
@@ -74,8 +78,7 @@ class Data:
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
hashed = self.hash_password(password)
|
||||
|
||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
@@ -84,22 +87,55 @@ class Data:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(hashed, hashed)
|
||||
# check a hash to make timing the same as if user was found
|
||||
bcrypt.checkpw(b'foo',
|
||||
dummy)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(hashed,
|
||||
base64.b64decode(found['password'])):
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# if the hash is not a bcrypt hash...
|
||||
# provide a transparant upgrade for old pbkdf2 hash format
|
||||
if not (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$')):
|
||||
# IMPORTANT! validate the login, bail if invalid
|
||||
hashed = self.legacy_hash_password(password)
|
||||
if not hmac.compare_digest(hashed, user_hash):
|
||||
raise InvalidAuth
|
||||
# then re-hash the valid password with bcrypt
|
||||
self.change_password(found['username'], password)
|
||||
run_coroutine_threadsafe(
|
||||
self.async_save(), self.hass.loop
|
||||
).result()
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# bcrypt.checkpw is timing-safe
|
||||
if not bcrypt.checkpw(password.encode(),
|
||||
user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
def legacy_hash_password(self, password: str,
|
||||
for_storage: bool = False) -> bytes:
|
||||
"""LEGACY password encoding."""
|
||||
# We're no longer storing salts in data, but if one exists we
|
||||
# should be able to retrieve it.
|
||||
salt = self._data['salt'].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
|
||||
# type: bytes
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -111,31 +111,19 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
try:
|
||||
cast(TrustedNetworksAuthProvider, self._auth_provider)\
|
||||
.async_validate_access(self._ip_address)
|
||||
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=None,
|
||||
errors=errors,
|
||||
return self.async_abort(
|
||||
reason='not_whitelisted'
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
user_id = user_input['user']
|
||||
if user_id not in self._available_users:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = {'user': vol.In(self._available_users)}
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
|
||||
)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -61,7 +59,6 @@ def from_config_dict(config: Dict[str, Any],
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
@@ -94,8 +91,13 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
try:
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, core_config, has_api_password, has_trusted_networks)
|
||||
except vol.Invalid as ex:
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
except vol.Invalid as config_err:
|
||||
conf_util.async_log_exception(
|
||||
config_err, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
except HomeAssistantError:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
return None
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
@@ -130,7 +132,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"further initialization aborted")
|
||||
"Further initialization aborted")
|
||||
return hass
|
||||
|
||||
await persistent_notification.async_setup(hass, config)
|
||||
@@ -156,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_ids
|
||||
})
|
||||
|
||||
|
||||
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,33 @@ 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."""
|
||||
tasks = [hass.helpers.entity_component.async_update_entity(entity)
|
||||
for entity in call.data[ATTR_ENTITY_ID]]
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
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, config, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
@@ -178,9 +172,9 @@ def async_setup(hass, config):
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
tasks = [_setup_atv(hass, config, 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, hass_config, 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] = {
|
||||
@@ -219,10 +212,10 @@ def _setup_atv(hass, atv_config):
|
||||
}
|
||||
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
hass, 'media_player', DOMAIN, atv_config, hass_config))
|
||||
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
hass, 'remote', DOMAIN, atv_config, hass_config))
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/ca.json
Normal file
35
homeassistant/components/auth/.translations/ca.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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 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": {
|
||||
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.",
|
||||
"title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/en.json
Normal file
35
homeassistant/components/auth/.translations/en.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
|
||||
"title": "Set up two-factor authentication using TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/ko.json
Normal file
35
homeassistant/components/auth/.translations/ko.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/lb.json
Normal file
35
homeassistant/components/auth/.translations/lb.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Fir d'Zwee-Faktor-Authentifikatioun m\u00ebttels engem Z\u00e4it bas\u00e9ierten eemolege Passwuert z'aktiv\u00e9ieren, scannt de QR Code mat enger Authentifikatioun's App.\nFalls dir keng hutt, recommand\u00e9iere mir entweder [Google Authenticator](https://support.google.com/accounts/answer/1066447) oder [Authy](https://authy.com/).\n\n{qr_code}\n\nNodeems de Code gescannt ass, gitt de sechs stellege Code vun der App a fir d'Konfiguratioun z'iwwerpr\u00e9iwen. Am Fall vu Problemer fir de QR Code ze scannen, gitt de folgende Code **`{code}`** a fir ee manuelle Setup.",
|
||||
"title": "Zwee Faktor Authentifikatioun mat TOTP konfigur\u00e9ieren"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/ru.json
Normal file
35
homeassistant/components/auth/.translations/ru.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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\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"
|
||||
}
|
||||
},
|
||||
"title": "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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/zh-Hans.json
Normal file
35
homeassistant/components/auth/.translations/zh-Hans.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u8981\u6fc0\u6d3b\u57fa\u4e8e\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u7684\u53cc\u91cd\u8ba4\u8bc1\uff0c\u8bf7\u7528\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\u626b\u63cf\u4ee5\u4e0b\u4e8c\u7ef4\u7801\u3002\u5982\u679c\u60a8\u8fd8\u6ca1\u6709\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\uff0c\u63a8\u8350\u4f7f\u7528 [Google \u8eab\u4efd\u9a8c\u8bc1\u5668](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u626b\u63cf\u4e8c\u7ef4\u7801\u4ee5\u540e\uff0c\u8f93\u5165\u5e94\u7528\u4e0a\u7684\u516d\u4f4d\u6570\u5b57\u53e3\u4ee4\u6765\u9a8c\u8bc1\u914d\u7f6e\u3002\u5982\u679c\u5728\u626b\u63cf\u4e8c\u7ef4\u7801\u65f6\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u4f7f\u7528\u4ee3\u7801 **`{code}`** \u624b\u52a8\u914d\u7f6e\u3002",
|
||||
"title": "\u7528\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u8bbe\u7f6e\u53cc\u91cd\u8ba4\u8bc1"
|
||||
}
|
||||
},
|
||||
"title": "\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
homeassistant/components/auth/.translations/zh-Hant.json
Normal file
35
homeassistant/components/auth/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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\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"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'))
|
||||
|
||||
|
||||
35
homeassistant/components/auth/strings.json
Normal file
35
homeassistant/components/auth/strings.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"mfa_setup":{
|
||||
"totp": {
|
||||
"title": "TOTP",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
}
|
||||
},
|
||||
"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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user