mirror of
https://github.com/home-assistant/core.git
synced 2025-11-03 07:59:30 +00:00
Compare commits
993 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9b8b290fc | ||
|
|
06b9600069 | ||
|
|
cff4755708 | ||
|
|
17f04c1736 | ||
|
|
0b6aa38b13 | ||
|
|
782f5c7d19 | ||
|
|
30fccc696e | ||
|
|
fb947288ad | ||
|
|
be3800d9a5 | ||
|
|
de79c42b8a | ||
|
|
b3bd59efb0 | ||
|
|
31737c5100 | ||
|
|
dbf6b01a60 | ||
|
|
b2bdf05cae | ||
|
|
461e6acf5c | ||
|
|
9bf824bf00 | ||
|
|
0009e7bde9 | ||
|
|
67e62e8020 | ||
|
|
e8e135fd25 | ||
|
|
63e53fdf15 | ||
|
|
ed3ca2b74f | ||
|
|
91a93b0060 | ||
|
|
6d3167fcd4 | ||
|
|
255607f3a5 | ||
|
|
9807ba1a5d | ||
|
|
782a90a535 | ||
|
|
7caddd48cd | ||
|
|
5c99862878 | ||
|
|
6a5f9faa33 | ||
|
|
03d94df3cd | ||
|
|
45484ba569 | ||
|
|
92c536ec0e | ||
|
|
1f290bad94 | ||
|
|
dd938d7460 | ||
|
|
73ed2ab164 | ||
|
|
283407fe6c | ||
|
|
97e928df4a | ||
|
|
a39846bad9 | ||
|
|
3fe895c18f | ||
|
|
d7efe274c1 | ||
|
|
91b7d56aa6 | ||
|
|
1895e03874 | ||
|
|
0a301f7dcb | ||
|
|
58c7ee649d | ||
|
|
d80dce31da | ||
|
|
94f24e6d49 | ||
|
|
2e169320a4 | ||
|
|
dce6a9f882 | ||
|
|
93689d68f7 | ||
|
|
5a802c1069 | ||
|
|
602b59aed8 | ||
|
|
6f8ac7f5c9 | ||
|
|
5910161202 | ||
|
|
afc70fda50 | ||
|
|
8613694544 | ||
|
|
cb7ae5cdf2 | ||
|
|
a4c0c34028 | ||
|
|
6eba7c4ff3 | ||
|
|
82edea6077 | ||
|
|
9b47af68ae | ||
|
|
02b46e2ba3 | ||
|
|
e9ae862fca | ||
|
|
31dc6832e7 | ||
|
|
c3e3f662f4 | ||
|
|
bcea3a9cba | ||
|
|
23290fa6ee | ||
|
|
4ee21e66dc | ||
|
|
4a3f754033 | ||
|
|
c75c00d568 | ||
|
|
a9361482d9 | ||
|
|
83e83520e6 | ||
|
|
65e6c50748 | ||
|
|
2a76347071 | ||
|
|
c5f9220500 | ||
|
|
afc109a585 | ||
|
|
a69c3953f1 | ||
|
|
a3e77bc5f3 | ||
|
|
957320f265 | ||
|
|
2fce79eccf | ||
|
|
caa48fab13 | ||
|
|
19ebdf2cf1 | ||
|
|
e9f96bfd7f | ||
|
|
ab8299b6cf | ||
|
|
1c5800d98b | ||
|
|
bfa86b8138 | ||
|
|
4163889c6b | ||
|
|
329d128e03 | ||
|
|
f516550f9f | ||
|
|
32ee4f0714 | ||
|
|
3d1a324f33 | ||
|
|
a9140dc8f5 | ||
|
|
145677ed75 | ||
|
|
7363378ac4 | ||
|
|
93706fa568 | ||
|
|
b763c0f902 | ||
|
|
1578187376 | ||
|
|
b12e79e5cf | ||
|
|
239e314dc1 | ||
|
|
e85e5789a2 | ||
|
|
9c840f93f0 | ||
|
|
dcc46226ee | ||
|
|
314e5ac296 | ||
|
|
9c77465c0e | ||
|
|
4073f63256 | ||
|
|
9565c0bd1d | ||
|
|
9cb5ea20af | ||
|
|
eef9246db1 | ||
|
|
865ea82432 | ||
|
|
4d9ef9e795 | ||
|
|
87bd2a32e4 | ||
|
|
f0693f6f91 | ||
|
|
3de822a0e2 | ||
|
|
71b56363d3 | ||
|
|
f400925825 | ||
|
|
20fb7b59ef | ||
|
|
622f23abd7 | ||
|
|
5337d0b4f3 | ||
|
|
2aabdf29bb | ||
|
|
b71dc752fa | ||
|
|
e16793013a | ||
|
|
98163504fb | ||
|
|
32cb666dac | ||
|
|
03dd1e6870 | ||
|
|
cb73a8bbb0 | ||
|
|
3169c0416e | ||
|
|
1e03f945b5 | ||
|
|
a91d894132 | ||
|
|
2f71f8908b | ||
|
|
c38a0f1bf0 | ||
|
|
deeb288daf | ||
|
|
d6913c6914 | ||
|
|
af5eacf303 | ||
|
|
a87a5d266e | ||
|
|
027f173a08 | ||
|
|
66d0fb7dbf | ||
|
|
3a8891d9ac | ||
|
|
04e0fd1d46 | ||
|
|
6ae345b01c | ||
|
|
b03e6050c5 | ||
|
|
98dfbf2565 | ||
|
|
3740424725 | ||
|
|
360addfb0b | ||
|
|
f4ac317d64 | ||
|
|
d1ef875132 | ||
|
|
96c5e4c507 | ||
|
|
851d7e22e7 | ||
|
|
e5c97fdcab | ||
|
|
3e6de21302 | ||
|
|
4579717317 | ||
|
|
3802fec568 | ||
|
|
b62b3b26f2 | ||
|
|
62752e0065 | ||
|
|
df65d2151d | ||
|
|
c9c707e368 | ||
|
|
0f877711a0 | ||
|
|
60080a529d | ||
|
|
38576e5b74 | ||
|
|
3f3955c1cd | ||
|
|
6cb735271f | ||
|
|
0acd4b28f9 | ||
|
|
b6a799499a | ||
|
|
cbadd64b28 | ||
|
|
29c9081ca1 | ||
|
|
1f07909a14 | ||
|
|
21686c9263 | ||
|
|
edf2974979 | ||
|
|
ea75e3bfa8 | ||
|
|
9cd8a86eb4 | ||
|
|
d6e4208665 | ||
|
|
a0d4595f78 | ||
|
|
649bc55a3b | ||
|
|
a22aad50e1 | ||
|
|
92e9c2aa72 | ||
|
|
2adf5918f5 | ||
|
|
21870e2167 | ||
|
|
6b7cbca04c | ||
|
|
8a4c78b69f | ||
|
|
cfbd84f450 | ||
|
|
9f146a3954 | ||
|
|
e28170a0a6 | ||
|
|
3175627363 | ||
|
|
86d7bc4962 | ||
|
|
434c848104 | ||
|
|
92bad453f2 | ||
|
|
644c33cc1e | ||
|
|
b7896491e3 | ||
|
|
dcf8aba150 | ||
|
|
cce8b1183f | ||
|
|
e276e899cf | ||
|
|
3f4798b5c3 | ||
|
|
714d44c503 | ||
|
|
121a59abe0 | ||
|
|
0c7b0bdb44 | ||
|
|
c4b2c2bfcf | ||
|
|
bc67115df3 | ||
|
|
af03390c4f | ||
|
|
a4773dc3e4 | ||
|
|
ad903f9917 | ||
|
|
c00da509a1 | ||
|
|
9e286d7c1f | ||
|
|
9e33398a7b | ||
|
|
47003fc04f | ||
|
|
b7b62a90e2 | ||
|
|
901c4f18cb | ||
|
|
43048962f2 | ||
|
|
77bf10e37c | ||
|
|
9c7d3c2a63 | ||
|
|
312d49caec | ||
|
|
b5284aa445 | ||
|
|
b6e8cafdea | ||
|
|
3c68db32d6 | ||
|
|
544a3b929f | ||
|
|
67d92c4f5d | ||
|
|
8bebfba21a | ||
|
|
5024a80d61 | ||
|
|
599390d985 | ||
|
|
577cf0991f | ||
|
|
aa157e17f9 | ||
|
|
bd23145331 | ||
|
|
a629e1bec2 | ||
|
|
ec7d33f277 | ||
|
|
c099c259ea | ||
|
|
a3ec37834b | ||
|
|
3d841681d7 | ||
|
|
295a004326 | ||
|
|
8de0824688 | ||
|
|
edc1cbdc32 | ||
|
|
1788eaf037 | ||
|
|
52974ff742 | ||
|
|
54d463e746 | ||
|
|
c7c0ed89c8 | ||
|
|
8283f50e22 | ||
|
|
86e67e4712 | ||
|
|
ad3d7c9523 | ||
|
|
a10fb94e9e | ||
|
|
08fe7c3ece | ||
|
|
4222f7562b | ||
|
|
6ac9677168 | ||
|
|
fc8af22191 | ||
|
|
0f69be117f | ||
|
|
2734a30f37 | ||
|
|
65a8882426 | ||
|
|
7be7a8de30 | ||
|
|
f9973696f3 | ||
|
|
9798ff019f | ||
|
|
f6f549dc3c | ||
|
|
4750656f1a | ||
|
|
011cc624b6 | ||
|
|
2d9a964953 | ||
|
|
87133a0e77 | ||
|
|
fe8dec27a3 | ||
|
|
b5323cd894 | ||
|
|
23316a8344 | ||
|
|
4a757b7994 | ||
|
|
37a667c2de | ||
|
|
86ecd7555a | ||
|
|
02f55b039c | ||
|
|
398ea40189 | ||
|
|
cf0bd6470a | ||
|
|
0723c7f5dc | ||
|
|
7def587c93 | ||
|
|
d5a5695411 | ||
|
|
277a9a3995 | ||
|
|
50f0eac7f3 | ||
|
|
44e35b7f52 | ||
|
|
324587b2db | ||
|
|
ad3d0c4e99 | ||
|
|
3014930371 | ||
|
|
b773a9049c | ||
|
|
4e8cd7281c | ||
|
|
fd9370da39 | ||
|
|
399f8a72c3 | ||
|
|
75e42acfe7 | ||
|
|
42a444712b | ||
|
|
61a96aecc0 | ||
|
|
96105ef6e7 | ||
|
|
0524c51c1a | ||
|
|
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 | ||
|
|
5613816476 | ||
|
|
e9c7fe184d | ||
|
|
41bb4760f7 | ||
|
|
ee3f17d5c7 | ||
|
|
18d37ff0fd | ||
|
|
7fe0d8b2f4 | ||
|
|
8b42d0c471 | ||
|
|
213171769d | ||
|
|
d5813cf167 | ||
|
|
3e59ffb33a | ||
|
|
a0a54dfd5b | ||
|
|
3bfe9e757e | ||
|
|
9fdf123a2f | ||
|
|
aeaf694552 | ||
|
|
3d1c8ee467 | ||
|
|
98b92c78c0 | ||
|
|
df67093441 | ||
|
|
c475a876ce | ||
|
|
90c18d1c15 | ||
|
|
78b6439ee6 | ||
|
|
092c146eae | ||
|
|
03de658d4d | ||
|
|
3ea8c25e1f | ||
|
|
6e01ea5929 | ||
|
|
c6ccbed828 | ||
|
|
e58836f99f | ||
|
|
874225dd67 | ||
|
|
fc6cc22b6d | ||
|
|
39ea9a8c90 | ||
|
|
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 | ||
|
|
4b30cbbf3b | ||
|
|
41ac2a3c73 | ||
|
|
b8257866f5 | ||
|
|
849a93e0a6 | ||
|
|
1c251009fe | ||
|
|
201fd4afee | ||
|
|
3e0c6c176a | ||
|
|
44fdfdf695 | ||
|
|
fea1c921fc | ||
|
|
5e3e441aa0 | ||
|
|
a1e6e04a5e | ||
|
|
2002497d09 | ||
|
|
baeb791d84 | ||
|
|
9c9df793b4 | ||
|
|
05922ac56a | ||
|
|
34deaf8849 | ||
|
|
cc38981a38 | ||
|
|
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 | ||
|
|
4efe86327d | ||
|
|
ff78a5b04b | ||
|
|
453cbb7c60 | ||
|
|
77026a2242 | ||
|
|
501f2b0a93 | ||
|
|
117ea9e553 | ||
|
|
beed82ab12 | ||
|
|
601f2df5a7 | ||
|
|
34d369ba26 | ||
|
|
e2465da7c2 | ||
|
|
6bd120ff1d | ||
|
|
188f5de5ae | ||
|
|
06af76404f | ||
|
|
629c4a0bf5 | ||
|
|
3f06b4eb9b | ||
|
|
0db13a99aa | ||
|
|
4e3faf6108 | ||
|
|
9583947012 | ||
|
|
50fb59477a | ||
|
|
20f6cb7cc7 | ||
|
|
6b08e6e769 | ||
|
|
ee696643cd | ||
|
|
a059cc860a | ||
|
|
cfe5db4350 | ||
|
|
dcd7b9a529 | ||
|
|
f858938ada | ||
|
|
e96635b5c1 | ||
|
|
d2d715faa8 | ||
|
|
99f7b7f42d | ||
|
|
6433f2e2e3 | ||
|
|
f4a54e2483 | ||
|
|
85c40d29ee | ||
|
|
a4a175440b | ||
|
|
8bec4a55d1 | ||
|
|
50f63ed4c5 | ||
|
|
c9e34e236d | ||
|
|
38318ed15f | ||
|
|
553d3c0179 | ||
|
|
20879726b0 | ||
|
|
3db766e2ec | ||
|
|
1eac6408f5 | ||
|
|
e73f31d829 | ||
|
|
839b58c600 | ||
|
|
71d00062e5 | ||
|
|
80f98b9ea2 |
78
.coveragerc
78
.coveragerc
@@ -28,6 +28,9 @@ omit =
|
|||||||
homeassistant/components/apple_tv.py
|
homeassistant/components/apple_tv.py
|
||||||
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
|
||||||
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_mbox.py
|
homeassistant/components/*/asterisk_mbox.py
|
||||||
|
homeassistant/components/*/asterisk_cdr.py
|
||||||
|
|
||||||
homeassistant/components/august.py
|
homeassistant/components/august.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/*/bbb_gpio.py
|
homeassistant/components/*/bbb_gpio.py
|
||||||
|
|
||||||
homeassistant/components/blink.py
|
homeassistant/components/blink/*
|
||||||
homeassistant/components/*/blink.py
|
homeassistant/components/*/blink.py
|
||||||
|
|
||||||
homeassistant/components/bloomsky.py
|
homeassistant/components/bloomsky.py
|
||||||
@@ -72,16 +76,13 @@ omit =
|
|||||||
homeassistant/components/daikin.py
|
homeassistant/components/daikin.py
|
||||||
homeassistant/components/*/daikin.py
|
homeassistant/components/*/daikin.py
|
||||||
|
|
||||||
homeassistant/components/deconz/*
|
|
||||||
homeassistant/components/*/deconz.py
|
|
||||||
|
|
||||||
homeassistant/components/digital_ocean.py
|
homeassistant/components/digital_ocean.py
|
||||||
homeassistant/components/*/digital_ocean.py
|
homeassistant/components/*/digital_ocean.py
|
||||||
|
|
||||||
homeassistant/components/dominos.py
|
homeassistant/components/dominos.py
|
||||||
|
|
||||||
homeassistant/components/doorbird.py
|
homeassistant/components/doorbird.py
|
||||||
homeassistant/components/*/doorbird.py
|
homeassistant/components/*/doorbird.py
|
||||||
|
|
||||||
homeassistant/components/dweet.py
|
homeassistant/components/dweet.py
|
||||||
homeassistant/components/*/dweet.py
|
homeassistant/components/*/dweet.py
|
||||||
@@ -92,17 +93,26 @@ omit =
|
|||||||
homeassistant/components/ecobee.py
|
homeassistant/components/ecobee.py
|
||||||
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/*/egardia.py
|
homeassistant/components/*/egardia.py
|
||||||
|
|
||||||
|
homeassistant/components/elkm1/*
|
||||||
|
homeassistant/components/*/elkm1.py
|
||||||
|
|
||||||
homeassistant/components/enocean.py
|
homeassistant/components/enocean.py
|
||||||
homeassistant/components/*/enocean.py
|
homeassistant/components/*/enocean.py
|
||||||
|
|
||||||
homeassistant/components/envisalink.py
|
homeassistant/components/envisalink.py
|
||||||
homeassistant/components/*/envisalink.py
|
homeassistant/components/*/envisalink.py
|
||||||
|
|
||||||
|
homeassistant/components/evohome.py
|
||||||
|
homeassistant/components/*/evohome.py
|
||||||
|
|
||||||
homeassistant/components/fritzbox.py
|
homeassistant/components/fritzbox.py
|
||||||
homeassistant/components/switch/fritzbox.py
|
homeassistant/components/*/fritzbox.py
|
||||||
|
|
||||||
homeassistant/components/ecovacs.py
|
homeassistant/components/ecovacs.py
|
||||||
homeassistant/components/*/ecovacs.py
|
homeassistant/components/*/ecovacs.py
|
||||||
@@ -116,6 +126,9 @@ omit =
|
|||||||
homeassistant/components/google.py
|
homeassistant/components/google.py
|
||||||
homeassistant/components/*/google.py
|
homeassistant/components/*/google.py
|
||||||
|
|
||||||
|
homeassistant/components/greeneye_monitor.py
|
||||||
|
homeassistant/components/sensor/greeneye_monitor.py
|
||||||
|
|
||||||
homeassistant/components/habitica/*
|
homeassistant/components/habitica/*
|
||||||
homeassistant/components/*/habitica.py
|
homeassistant/components/*/habitica.py
|
||||||
|
|
||||||
@@ -123,6 +136,7 @@ omit =
|
|||||||
homeassistant/components/hangouts/const.py
|
homeassistant/components/hangouts/const.py
|
||||||
homeassistant/components/hangouts/hangouts_bot.py
|
homeassistant/components/hangouts/hangouts_bot.py
|
||||||
homeassistant/components/hangouts/hangups_utils.py
|
homeassistant/components/hangouts/hangups_utils.py
|
||||||
|
homeassistant/components/hangouts/intents.py
|
||||||
homeassistant/components/*/hangouts.py
|
homeassistant/components/*/hangouts.py
|
||||||
|
|
||||||
homeassistant/components/hdmi_cec.py
|
homeassistant/components/hdmi_cec.py
|
||||||
@@ -140,6 +154,9 @@ omit =
|
|||||||
homeassistant/components/homematicip_cloud.py
|
homeassistant/components/homematicip_cloud.py
|
||||||
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/*/hydrawise.py
|
homeassistant/components/*/hydrawise.py
|
||||||
|
|
||||||
@@ -183,13 +200,15 @@ omit =
|
|||||||
homeassistant/components/linode.py
|
homeassistant/components/linode.py
|
||||||
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
|
||||||
homeassistant/components/*/lutron.py
|
homeassistant/components/*/lutron.py
|
||||||
|
|
||||||
homeassistant/components/lutron_caseta.py
|
homeassistant/components/lutron_caseta.py
|
||||||
homeassistant/components/*/lutron_caseta.py
|
homeassistant/components/*/lutron_caseta.py
|
||||||
|
|
||||||
homeassistant/components/mailgun.py
|
|
||||||
homeassistant/components/*/mailgun.py
|
homeassistant/components/*/mailgun.py
|
||||||
|
|
||||||
homeassistant/components/matrix.py
|
homeassistant/components/matrix.py
|
||||||
@@ -228,6 +247,9 @@ omit =
|
|||||||
homeassistant/components/opencv.py
|
homeassistant/components/opencv.py
|
||||||
homeassistant/components/*/opencv.py
|
homeassistant/components/*/opencv.py
|
||||||
|
|
||||||
|
homeassistant/components/opentherm_gw/*
|
||||||
|
homeassistant/components/*/opentherm_gw.py
|
||||||
|
|
||||||
homeassistant/components/openuv/__init__.py
|
homeassistant/components/openuv/__init__.py
|
||||||
homeassistant/components/*/openuv.py
|
homeassistant/components/*/openuv.py
|
||||||
|
|
||||||
@@ -267,6 +289,12 @@ omit =
|
|||||||
homeassistant/components/scsgate.py
|
homeassistant/components/scsgate.py
|
||||||
homeassistant/components/*/scsgate.py
|
homeassistant/components/*/scsgate.py
|
||||||
|
|
||||||
|
homeassistant/components/sense.py
|
||||||
|
homeassistant/components/*/sense.py
|
||||||
|
|
||||||
|
homeassistant/components/simplisafe/__init__.py
|
||||||
|
homeassistant/components/*/simplisafe.py
|
||||||
|
|
||||||
homeassistant/components/sisyphus.py
|
homeassistant/components/sisyphus.py
|
||||||
homeassistant/components/*/sisyphus.py
|
homeassistant/components/*/sisyphus.py
|
||||||
|
|
||||||
@@ -299,13 +327,15 @@ omit =
|
|||||||
|
|
||||||
homeassistant/components/*/thinkingcleaner.py
|
homeassistant/components/*/thinkingcleaner.py
|
||||||
|
|
||||||
|
homeassistant/components/tibber/*
|
||||||
|
homeassistant/components/*/tibber.py
|
||||||
|
|
||||||
homeassistant/components/toon.py
|
homeassistant/components/toon.py
|
||||||
homeassistant/components/*/toon.py
|
homeassistant/components/*/toon.py
|
||||||
|
|
||||||
homeassistant/components/tradfri.py
|
homeassistant/components/tradfri.py
|
||||||
homeassistant/components/*/tradfri.py
|
homeassistant/components/*/tradfri.py
|
||||||
|
|
||||||
homeassistant/components/twilio.py
|
|
||||||
homeassistant/components/notify/twilio_sms.py
|
homeassistant/components/notify/twilio_sms.py
|
||||||
homeassistant/components/notify/twilio_call.py
|
homeassistant/components/notify/twilio_call.py
|
||||||
|
|
||||||
@@ -359,7 +389,7 @@ omit =
|
|||||||
homeassistant/components/zigbee.py
|
homeassistant/components/zigbee.py
|
||||||
homeassistant/components/*/zigbee.py
|
homeassistant/components/*/zigbee.py
|
||||||
|
|
||||||
homeassistant/components/zoneminder.py
|
homeassistant/components/zoneminder/*
|
||||||
homeassistant/components/*/zoneminder.py
|
homeassistant/components/*/zoneminder.py
|
||||||
|
|
||||||
homeassistant/components/tuya.py
|
homeassistant/components/tuya.py
|
||||||
@@ -375,7 +405,6 @@ omit =
|
|||||||
homeassistant/components/alarm_control_panel/ifttt.py
|
homeassistant/components/alarm_control_panel/ifttt.py
|
||||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.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/totalconnect.py
|
||||||
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
||||||
homeassistant/components/apiai.py
|
homeassistant/components/apiai.py
|
||||||
@@ -406,7 +435,6 @@ omit =
|
|||||||
homeassistant/components/camera/xeoma.py
|
homeassistant/components/camera/xeoma.py
|
||||||
homeassistant/components/camera/xiaomi.py
|
homeassistant/components/camera/xiaomi.py
|
||||||
homeassistant/components/camera/yi.py
|
homeassistant/components/camera/yi.py
|
||||||
homeassistant/components/climate/econet.py
|
|
||||||
homeassistant/components/climate/ephember.py
|
homeassistant/components/climate/ephember.py
|
||||||
homeassistant/components/climate/eq3btsmart.py
|
homeassistant/components/climate/eq3btsmart.py
|
||||||
homeassistant/components/climate/flexit.py
|
homeassistant/components/climate/flexit.py
|
||||||
@@ -414,8 +442,8 @@ omit =
|
|||||||
homeassistant/components/climate/homematic.py
|
homeassistant/components/climate/homematic.py
|
||||||
homeassistant/components/climate/honeywell.py
|
homeassistant/components/climate/honeywell.py
|
||||||
homeassistant/components/climate/knx.py
|
homeassistant/components/climate/knx.py
|
||||||
|
homeassistant/components/climate/mill.py
|
||||||
homeassistant/components/climate/oem.py
|
homeassistant/components/climate/oem.py
|
||||||
homeassistant/components/climate/opentherm_gw.py
|
|
||||||
homeassistant/components/climate/proliphix.py
|
homeassistant/components/climate/proliphix.py
|
||||||
homeassistant/components/climate/radiotherm.py
|
homeassistant/components/climate/radiotherm.py
|
||||||
homeassistant/components/climate/sensibo.py
|
homeassistant/components/climate/sensibo.py
|
||||||
@@ -431,7 +459,6 @@ omit =
|
|||||||
homeassistant/components/cover/myq.py
|
homeassistant/components/cover/myq.py
|
||||||
homeassistant/components/cover/opengarage.py
|
homeassistant/components/cover/opengarage.py
|
||||||
homeassistant/components/cover/rpi_gpio.py
|
homeassistant/components/cover/rpi_gpio.py
|
||||||
homeassistant/components/cover/ryobi_gdo.py
|
|
||||||
homeassistant/components/cover/scsgate.py
|
homeassistant/components/cover/scsgate.py
|
||||||
homeassistant/components/device_tracker/actiontec.py
|
homeassistant/components/device_tracker/actiontec.py
|
||||||
homeassistant/components/device_tracker/aruba.py
|
homeassistant/components/device_tracker/aruba.py
|
||||||
@@ -441,6 +468,7 @@ omit =
|
|||||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||||
|
homeassistant/components/device_tracker/bt_smarthub.py
|
||||||
homeassistant/components/device_tracker/cisco_ios.py
|
homeassistant/components/device_tracker/cisco_ios.py
|
||||||
homeassistant/components/device_tracker/ddwrt.py
|
homeassistant/components/device_tracker/ddwrt.py
|
||||||
homeassistant/components/device_tracker/freebox.py
|
homeassistant/components/device_tracker/freebox.py
|
||||||
@@ -458,6 +486,7 @@ omit =
|
|||||||
homeassistant/components/device_tracker/netgear.py
|
homeassistant/components/device_tracker/netgear.py
|
||||||
homeassistant/components/device_tracker/nmap_tracker.py
|
homeassistant/components/device_tracker/nmap_tracker.py
|
||||||
homeassistant/components/device_tracker/ping.py
|
homeassistant/components/device_tracker/ping.py
|
||||||
|
homeassistant/components/device_tracker/quantum_gateway.py
|
||||||
homeassistant/components/device_tracker/ritassist.py
|
homeassistant/components/device_tracker/ritassist.py
|
||||||
homeassistant/components/device_tracker/sky_hub.py
|
homeassistant/components/device_tracker/sky_hub.py
|
||||||
homeassistant/components/device_tracker/snmp.py
|
homeassistant/components/device_tracker/snmp.py
|
||||||
@@ -473,6 +502,7 @@ omit =
|
|||||||
homeassistant/components/emoncms_history.py
|
homeassistant/components/emoncms_history.py
|
||||||
homeassistant/components/emulated_hue/upnp.py
|
homeassistant/components/emulated_hue/upnp.py
|
||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
|
homeassistant/components/fan/wemo.py
|
||||||
homeassistant/components/folder_watcher.py
|
homeassistant/components/folder_watcher.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
homeassistant/components/goalfeed.py
|
homeassistant/components/goalfeed.py
|
||||||
@@ -480,6 +510,7 @@ omit =
|
|||||||
homeassistant/components/image_processing/dlib_face_detect.py
|
homeassistant/components/image_processing/dlib_face_detect.py
|
||||||
homeassistant/components/image_processing/dlib_face_identify.py
|
homeassistant/components/image_processing/dlib_face_identify.py
|
||||||
homeassistant/components/image_processing/seven_segments.py
|
homeassistant/components/image_processing/seven_segments.py
|
||||||
|
homeassistant/components/image_processing/tensorflow.py
|
||||||
homeassistant/components/keyboard_remote.py
|
homeassistant/components/keyboard_remote.py
|
||||||
homeassistant/components/keyboard.py
|
homeassistant/components/keyboard.py
|
||||||
homeassistant/components/light/avion.py
|
homeassistant/components/light/avion.py
|
||||||
@@ -499,6 +530,7 @@ omit =
|
|||||||
homeassistant/components/light/lw12wifi.py
|
homeassistant/components/light/lw12wifi.py
|
||||||
homeassistant/components/light/mystrom.py
|
homeassistant/components/light/mystrom.py
|
||||||
homeassistant/components/light/nanoleaf_aurora.py
|
homeassistant/components/light/nanoleaf_aurora.py
|
||||||
|
homeassistant/components/light/opple.py
|
||||||
homeassistant/components/light/osramlightify.py
|
homeassistant/components/light/osramlightify.py
|
||||||
homeassistant/components/light/piglow.py
|
homeassistant/components/light/piglow.py
|
||||||
homeassistant/components/light/rpi_gpio_pwm.py
|
homeassistant/components/light/rpi_gpio_pwm.py
|
||||||
@@ -540,6 +572,7 @@ omit =
|
|||||||
homeassistant/components/media_player/itunes.py
|
homeassistant/components/media_player/itunes.py
|
||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/lg_netcast.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/liveboxplaytv.py
|
||||||
homeassistant/components/media_player/mediaroom.py
|
homeassistant/components/media_player/mediaroom.py
|
||||||
homeassistant/components/media_player/mpchc.py
|
homeassistant/components/media_player/mpchc.py
|
||||||
@@ -583,6 +616,7 @@ omit =
|
|||||||
homeassistant/components/notify/gntp.py
|
homeassistant/components/notify/gntp.py
|
||||||
homeassistant/components/notify/group.py
|
homeassistant/components/notify/group.py
|
||||||
homeassistant/components/notify/hipchat.py
|
homeassistant/components/notify/hipchat.py
|
||||||
|
homeassistant/components/notify/homematic.py
|
||||||
homeassistant/components/notify/instapush.py
|
homeassistant/components/notify/instapush.py
|
||||||
homeassistant/components/notify/kodi.py
|
homeassistant/components/notify/kodi.py
|
||||||
homeassistant/components/notify/lannouncer.py
|
homeassistant/components/notify/lannouncer.py
|
||||||
@@ -609,13 +643,13 @@ omit =
|
|||||||
homeassistant/components/notify/telstra.py
|
homeassistant/components/notify/telstra.py
|
||||||
homeassistant/components/notify/twitter.py
|
homeassistant/components/notify/twitter.py
|
||||||
homeassistant/components/notify/xmpp.py
|
homeassistant/components/notify/xmpp.py
|
||||||
homeassistant/components/notify/yessssms.py
|
|
||||||
homeassistant/components/nuimo_controller.py
|
homeassistant/components/nuimo_controller.py
|
||||||
homeassistant/components/prometheus.py
|
homeassistant/components/prometheus.py
|
||||||
homeassistant/components/rainbird.py
|
homeassistant/components/rainbird.py
|
||||||
homeassistant/components/remember_the_milk/__init__.py
|
homeassistant/components/remember_the_milk/__init__.py
|
||||||
homeassistant/components/remote/harmony.py
|
homeassistant/components/remote/harmony.py
|
||||||
homeassistant/components/remote/itach.py
|
homeassistant/components/remote/itach.py
|
||||||
|
homeassistant/components/route53.py
|
||||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||||
homeassistant/components/scene/lifx_cloud.py
|
homeassistant/components/scene/lifx_cloud.py
|
||||||
homeassistant/components/sensor/airvisual.py
|
homeassistant/components/sensor/airvisual.py
|
||||||
@@ -668,6 +702,7 @@ omit =
|
|||||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||||
homeassistant/components/sensor/gearbest.py
|
homeassistant/components/sensor/gearbest.py
|
||||||
homeassistant/components/sensor/geizhals.py
|
homeassistant/components/sensor/geizhals.py
|
||||||
|
homeassistant/components/sensor/gitlab_ci.py
|
||||||
homeassistant/components/sensor/gitter.py
|
homeassistant/components/sensor/gitter.py
|
||||||
homeassistant/components/sensor/glances.py
|
homeassistant/components/sensor/glances.py
|
||||||
homeassistant/components/sensor/google_travel_time.py
|
homeassistant/components/sensor/google_travel_time.py
|
||||||
@@ -676,6 +711,7 @@ omit =
|
|||||||
homeassistant/components/sensor/haveibeenpwned.py
|
homeassistant/components/sensor/haveibeenpwned.py
|
||||||
homeassistant/components/sensor/hp_ilo.py
|
homeassistant/components/sensor/hp_ilo.py
|
||||||
homeassistant/components/sensor/htu21d.py
|
homeassistant/components/sensor/htu21d.py
|
||||||
|
homeassistant/components/sensor/upnp.py
|
||||||
homeassistant/components/sensor/imap_email_content.py
|
homeassistant/components/sensor/imap_email_content.py
|
||||||
homeassistant/components/sensor/imap.py
|
homeassistant/components/sensor/imap.py
|
||||||
homeassistant/components/sensor/influxdb.py
|
homeassistant/components/sensor/influxdb.py
|
||||||
@@ -684,11 +720,13 @@ omit =
|
|||||||
homeassistant/components/sensor/kwb.py
|
homeassistant/components/sensor/kwb.py
|
||||||
homeassistant/components/sensor/lacrosse.py
|
homeassistant/components/sensor/lacrosse.py
|
||||||
homeassistant/components/sensor/lastfm.py
|
homeassistant/components/sensor/lastfm.py
|
||||||
|
homeassistant/components/sensor/linky.py
|
||||||
homeassistant/components/sensor/linux_battery.py
|
homeassistant/components/sensor/linux_battery.py
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
homeassistant/components/sensor/luftdaten.py
|
homeassistant/components/sensor/luftdaten.py
|
||||||
homeassistant/components/sensor/lyft.py
|
homeassistant/components/sensor/lyft.py
|
||||||
homeassistant/components/sensor/magicseaweed.py
|
homeassistant/components/sensor/magicseaweed.py
|
||||||
|
homeassistant/components/sensor/meteo_france.py
|
||||||
homeassistant/components/sensor/metoffice.py
|
homeassistant/components/sensor/metoffice.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/mitemp_bt.py
|
homeassistant/components/sensor/mitemp_bt.py
|
||||||
@@ -697,6 +735,7 @@ omit =
|
|||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
homeassistant/components/sensor/mvglive.py
|
homeassistant/components/sensor/mvglive.py
|
||||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||||
|
homeassistant/components/sensor/netatmo_public.py
|
||||||
homeassistant/components/sensor/netdata.py
|
homeassistant/components/sensor/netdata.py
|
||||||
homeassistant/components/sensor/netdata_public.py
|
homeassistant/components/sensor/netdata_public.py
|
||||||
homeassistant/components/sensor/neurio_energy.py
|
homeassistant/components/sensor/neurio_energy.py
|
||||||
@@ -723,8 +762,8 @@ omit =
|
|||||||
homeassistant/components/sensor/radarr.py
|
homeassistant/components/sensor/radarr.py
|
||||||
homeassistant/components/sensor/rainbird.py
|
homeassistant/components/sensor/rainbird.py
|
||||||
homeassistant/components/sensor/ripple.py
|
homeassistant/components/sensor/ripple.py
|
||||||
|
homeassistant/components/sensor/rtorrent.py
|
||||||
homeassistant/components/sensor/scrape.py
|
homeassistant/components/sensor/scrape.py
|
||||||
homeassistant/components/sensor/sense.py
|
|
||||||
homeassistant/components/sensor/sensehat.py
|
homeassistant/components/sensor/sensehat.py
|
||||||
homeassistant/components/sensor/serial_pm.py
|
homeassistant/components/sensor/serial_pm.py
|
||||||
homeassistant/components/sensor/serial.py
|
homeassistant/components/sensor/serial.py
|
||||||
@@ -740,6 +779,7 @@ omit =
|
|||||||
homeassistant/components/sensor/sonarr.py
|
homeassistant/components/sensor/sonarr.py
|
||||||
homeassistant/components/sensor/speedtest.py
|
homeassistant/components/sensor/speedtest.py
|
||||||
homeassistant/components/sensor/spotcrime.py
|
homeassistant/components/sensor/spotcrime.py
|
||||||
|
homeassistant/components/sensor/starlingbank.py
|
||||||
homeassistant/components/sensor/steam_online.py
|
homeassistant/components/sensor/steam_online.py
|
||||||
homeassistant/components/sensor/supervisord.py
|
homeassistant/components/sensor/supervisord.py
|
||||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||||
@@ -751,7 +791,7 @@ omit =
|
|||||||
homeassistant/components/sensor/tank_utility.py
|
homeassistant/components/sensor/tank_utility.py
|
||||||
homeassistant/components/sensor/ted5000.py
|
homeassistant/components/sensor/ted5000.py
|
||||||
homeassistant/components/sensor/temper.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/time_date.py
|
||||||
homeassistant/components/sensor/torque.py
|
homeassistant/components/sensor/torque.py
|
||||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||||
@@ -759,7 +799,6 @@ omit =
|
|||||||
homeassistant/components/sensor/travisci.py
|
homeassistant/components/sensor/travisci.py
|
||||||
homeassistant/components/sensor/twitch.py
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/uber.py
|
homeassistant/components/sensor/uber.py
|
||||||
homeassistant/components/sensor/upnp.py
|
|
||||||
homeassistant/components/sensor/ups.py
|
homeassistant/components/sensor/ups.py
|
||||||
homeassistant/components/sensor/uscis.py
|
homeassistant/components/sensor/uscis.py
|
||||||
homeassistant/components/sensor/vasttrafik.py
|
homeassistant/components/sensor/vasttrafik.py
|
||||||
@@ -793,6 +832,7 @@ omit =
|
|||||||
homeassistant/components/switch/pulseaudio_loopback.py
|
homeassistant/components/switch/pulseaudio_loopback.py
|
||||||
homeassistant/components/switch/rainbird.py
|
homeassistant/components/switch/rainbird.py
|
||||||
homeassistant/components/switch/rest.py
|
homeassistant/components/switch/rest.py
|
||||||
|
homeassistant/components/switch/recswitch.py
|
||||||
homeassistant/components/switch/rpi_rf.py
|
homeassistant/components/switch/rpi_rf.py
|
||||||
homeassistant/components/switch/snmp.py
|
homeassistant/components/switch/snmp.py
|
||||||
homeassistant/components/switch/switchbot.py
|
homeassistant/components/switch/switchbot.py
|
||||||
@@ -809,10 +849,12 @@ omit =
|
|||||||
homeassistant/components/tts/picotts.py
|
homeassistant/components/tts/picotts.py
|
||||||
homeassistant/components/vacuum/mqtt.py
|
homeassistant/components/vacuum/mqtt.py
|
||||||
homeassistant/components/vacuum/roomba.py
|
homeassistant/components/vacuum/roomba.py
|
||||||
|
homeassistant/components/water_heater/econet.py
|
||||||
homeassistant/components/watson_iot.py
|
homeassistant/components/watson_iot.py
|
||||||
homeassistant/components/weather/bom.py
|
homeassistant/components/weather/bom.py
|
||||||
homeassistant/components/weather/buienradar.py
|
homeassistant/components/weather/buienradar.py
|
||||||
homeassistant/components/weather/darksky.py
|
homeassistant/components/weather/darksky.py
|
||||||
|
homeassistant/components/weather/met.py
|
||||||
homeassistant/components/weather/metoffice.py
|
homeassistant/components/weather/metoffice.py
|
||||||
homeassistant/components/weather/openweathermap.py
|
homeassistant/components/weather/openweathermap.py
|
||||||
homeassistant/components/weather/zamg.py
|
homeassistant/components/weather/zamg.py
|
||||||
|
|||||||
10
.readthedocs.yml
Normal file
10
.readthedocs.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# .readthedocs.yml
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: latest
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.6
|
||||||
|
setup_py_install: true
|
||||||
|
|
||||||
|
requirements_file: requirements_docs.txt
|
||||||
158
CODEOWNERS
158
CODEOWNERS
@@ -2,60 +2,75 @@
|
|||||||
# when the code that they own is touched.
|
# when the code that they own is touched.
|
||||||
# https://github.com/blog/2392-introducing-code-owners
|
# https://github.com/blog/2392-introducing-code-owners
|
||||||
|
|
||||||
|
# Home Assistant Core
|
||||||
setup.py @home-assistant/core
|
setup.py @home-assistant/core
|
||||||
homeassistant/*.py @home-assistant/core
|
homeassistant/*.py @home-assistant/core
|
||||||
homeassistant/helpers/* @home-assistant/core
|
homeassistant/helpers/* @home-assistant/core
|
||||||
homeassistant/util/* @home-assistant/core
|
homeassistant/util/* @home-assistant/core
|
||||||
homeassistant/components/api.py @home-assistant/core
|
homeassistant/components/api.py @home-assistant/core
|
||||||
|
homeassistant/components/auth/* @home-assistant/core
|
||||||
homeassistant/components/automation/* @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/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/history.py @home-assistant/core
|
||||||
homeassistant/components/http/* @home-assistant/core
|
homeassistant/components/http/* @home-assistant/core
|
||||||
homeassistant/components/input_*.py @home-assistant/core
|
homeassistant/components/input_*.py @home-assistant/core
|
||||||
homeassistant/components/introduction.py @home-assistant/core
|
homeassistant/components/introduction.py @home-assistant/core
|
||||||
homeassistant/components/logger.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/mqtt/* @home-assistant/core
|
||||||
homeassistant/components/panel_custom.py @home-assistant/core
|
homeassistant/components/panel_custom.py @home-assistant/core
|
||||||
homeassistant/components/panel_iframe.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/__init__.py @home-assistant/core
|
||||||
homeassistant/components/scene/hass.py @home-assistant/core
|
homeassistant/components/scene/hass.py @home-assistant/core
|
||||||
homeassistant/components/script.py @home-assistant/core
|
homeassistant/components/script.py @home-assistant/core
|
||||||
homeassistant/components/shell_command.py @home-assistant/core
|
homeassistant/components/shell_command.py @home-assistant/core
|
||||||
homeassistant/components/sun.py @home-assistant/core
|
homeassistant/components/sun.py @home-assistant/core
|
||||||
homeassistant/components/updater.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/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
|
Dockerfile @home-assistant/docker
|
||||||
virtualization/Docker/* @home-assistant/docker
|
virtualization/Docker/* @home-assistant/docker
|
||||||
|
|
||||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||||
homeassistant/components/*/zwave.py @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/egardia.py @jeroenterheerdt
|
||||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
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/camera/yi.py @bachya
|
||||||
homeassistant/components/climate/ephember.py @ttroy50
|
homeassistant/components/climate/ephember.py @ttroy50
|
||||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||||
|
homeassistant/components/climate/mill.py @danielhiversen
|
||||||
homeassistant/components/climate/sensibo.py @andrey-git
|
homeassistant/components/climate/sensibo.py @andrey-git
|
||||||
|
homeassistant/components/cover/brunt.py @eavanvalkenburg
|
||||||
homeassistant/components/cover/group.py @cdce8p
|
homeassistant/components/cover/group.py @cdce8p
|
||||||
homeassistant/components/cover/template.py @PhracturedBlue
|
homeassistant/components/cover/template.py @PhracturedBlue
|
||||||
|
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
|
||||||
homeassistant/components/device_tracker/automatic.py @armills
|
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/device_tracker/tile.py @bachya
|
||||||
|
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
|
||||||
homeassistant/components/history_graph.py @andrey-git
|
homeassistant/components/history_graph.py @andrey-git
|
||||||
homeassistant/components/light/lifx.py @amelchio
|
homeassistant/components/influx.py @fabaff
|
||||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||||
homeassistant/components/light/tplink.py @rytilahti
|
homeassistant/components/light/tplink.py @rytilahti
|
||||||
homeassistant/components/light/yeelight.py @rytilahti
|
homeassistant/components/light/yeelight.py @rytilahti
|
||||||
|
homeassistant/components/light/yeelightsunflower.py @lindsaymarkward
|
||||||
homeassistant/components/lock/nello.py @pschmitt
|
homeassistant/components/lock/nello.py @pschmitt
|
||||||
homeassistant/components/lock/nuki.py @pschmitt
|
homeassistant/components/lock/nuki.py @pschmitt
|
||||||
homeassistant/components/media_player/emby.py @mezz64
|
homeassistant/components/media_player/emby.py @mezz64
|
||||||
@@ -63,63 +78,178 @@ homeassistant/components/media_player/kodi.py @armills
|
|||||||
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||||
homeassistant/components/media_player/monoprice.py @etsinko
|
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/sonos.py @amelchio
|
||||||
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
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/notify/yessssms.py @flowolf
|
||||||
homeassistant/components/plant.py @ChristianKuehnel
|
homeassistant/components/plant.py @ChristianKuehnel
|
||||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||||
homeassistant/components/sensor/airvisual.py @bachya
|
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/filter.py @dgomes
|
||||||
|
homeassistant/components/sensor/fixer.py @fabaff
|
||||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
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/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/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/nsw_fuel_station.py @nickw444
|
||||||
|
homeassistant/components/sensor/pi_hole.py @fabaff
|
||||||
homeassistant/components/sensor/pollen.py @bachya
|
homeassistant/components/sensor/pollen.py @bachya
|
||||||
|
homeassistant/components/sensor/pvoutput.py @fabaff
|
||||||
homeassistant/components/sensor/qnap.py @colinodell
|
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/sma.py @kellerza
|
||||||
homeassistant/components/sensor/sql.py @dgomes
|
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/sytadin.py @gautric
|
||||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
homeassistant/components/sensor/time_data.py @fabaff
|
||||||
homeassistant/components/sensor/upnp.py @dgomes
|
homeassistant/components/sensor/version.py @fabaff
|
||||||
homeassistant/components/sensor/waqi.py @andrey-git
|
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/switch/tplink.py @rytilahti
|
||||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
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
|
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
|
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/*/bmw_connected_drive.py @ChristianKuehnel
|
||||||
homeassistant/components/*/broadlink.py @danielhiversen
|
homeassistant/components/*/broadlink.py @danielhiversen
|
||||||
|
|
||||||
|
# C
|
||||||
|
homeassistant/components/counter/* @fabaff
|
||||||
|
|
||||||
|
# D
|
||||||
homeassistant/components/*/deconz.py @kane610
|
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/*/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
|
||||||
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/*/hive.py @Rendili @KJonline
|
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||||
homeassistant/components/homekit/* @cdce8p
|
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/*/knx.py @Julius2342
|
homeassistant/components/*/knx.py @Julius2342
|
||||||
homeassistant/components/konnected.py @heythisisnate
|
homeassistant/components/konnected.py @heythisisnate
|
||||||
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/*/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
|
homeassistant/components/*/openuv.py @bachya
|
||||||
|
|
||||||
|
# Q
|
||||||
homeassistant/components/qwikswitch.py @kellerza
|
homeassistant/components/qwikswitch.py @kellerza
|
||||||
homeassistant/components/*/qwikswitch.py @kellerza
|
homeassistant/components/*/qwikswitch.py @kellerza
|
||||||
|
|
||||||
|
# R
|
||||||
homeassistant/components/rainmachine/* @bachya
|
homeassistant/components/rainmachine/* @bachya
|
||||||
homeassistant/components/*/rainmachine.py @bachya
|
homeassistant/components/*/rainmachine.py @bachya
|
||||||
|
homeassistant/components/*/random.py @fabaff
|
||||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
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/*/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/*/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
|
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
|
||||||
homeassistant/components/*/velux.py @Julius2342
|
homeassistant/components/*/velux.py @Julius2342
|
||||||
|
|
||||||
|
# W
|
||||||
|
homeassistant/components/wemo.py @sqldiablo
|
||||||
|
homeassistant/components/*/wemo.py @sqldiablo
|
||||||
|
|
||||||
|
# X
|
||||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @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
|
homeassistant/scripts/check_config.py @kellerza
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ The process is straight-forward.
|
|||||||
- Ensure tests work.
|
- Ensure tests work.
|
||||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
|||||||
#ENV INSTALL_FFMPEG no
|
#ENV INSTALL_FFMPEG no
|
||||||
#ENV INSTALL_LIBCEC no
|
#ENV INSTALL_LIBCEC no
|
||||||
#ENV INSTALL_SSOCR no
|
#ENV INSTALL_SSOCR no
|
||||||
|
#ENV INSTALL_DLIB no
|
||||||
#ENV INSTALL_IPERF3 no
|
#ENV INSTALL_IPERF3 no
|
||||||
|
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
@@ -27,7 +28,7 @@ COPY requirements_all.txt requirements_all.txt
|
|||||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||||
# See PR #8103 for more info.
|
# See PR #8103 for more info.
|
||||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython
|
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython tensorflow
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ Featured integrations
|
|||||||
|
|
||||||
|screenshot-components|
|
|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
|
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://home-assistant.io/developers/creating_components/>`__.
|
components <https://developers.home-assistant.io/docs/en/creating_component_index.html>`__.
|
||||||
|
|
||||||
If you run into issues while using Home Assistant or during development
|
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.
|
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:`modindex`
|
||||||
* :ref:`search`
|
* :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 subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
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."""
|
"""Attempt to use uvloop."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from asyncio.events import BaseDefaultEventLoopPolicy
|
||||||
|
|
||||||
try:
|
policy = None
|
||||||
import uvloop
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
if sys.platform == 'win32':
|
||||||
except ImportError:
|
if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'):
|
||||||
pass
|
# 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:
|
def validate_python() -> None:
|
||||||
@@ -240,51 +258,39 @@ def cmdline() -> List[str]:
|
|||||||
return [arg for arg in sys.argv if arg != '--daemon']
|
return [arg for arg in sys.argv if arg != '--daemon']
|
||||||
|
|
||||||
|
|
||||||
def setup_and_run_hass(config_dir: str,
|
async def setup_and_run_hass(config_dir: str,
|
||||||
args: argparse.Namespace) -> int:
|
args: argparse.Namespace) -> int:
|
||||||
"""Set up HASS and run."""
|
"""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
|
hass = core.HomeAssistant()
|
||||||
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)
|
|
||||||
|
|
||||||
if args.demo_mode:
|
if args.demo_mode:
|
||||||
config = {
|
config = {
|
||||||
'frontend': {},
|
'frontend': {},
|
||||||
'demo': {}
|
'demo': {}
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
hass = bootstrap.from_config_dict(
|
bootstrap.async_from_config_dict(
|
||||||
config, config_dir=config_dir, verbose=args.verbose,
|
config, hass, config_dir=config_dir, verbose=args.verbose,
|
||||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||||
else:
|
else:
|
||||||
config_file = ensure_config_file(config_dir)
|
config_file = ensure_config_file(config_dir)
|
||||||
print('Config directory:', config_dir)
|
print('Config directory:', config_dir)
|
||||||
hass = bootstrap.from_config_file(
|
await bootstrap.async_from_config_file(
|
||||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
||||||
log_no_color=args.log_no_color)
|
log_no_color=args.log_no_color)
|
||||||
|
|
||||||
if hass is None:
|
|
||||||
return -1
|
|
||||||
|
|
||||||
if args.open_ui:
|
if args.open_ui:
|
||||||
# Imported here to avoid importing asyncio before monkey patch
|
# Imported here to avoid importing asyncio before monkey patch
|
||||||
from homeassistant.util.async_ import run_callback_threadsafe
|
from homeassistant.util.async_ import run_callback_threadsafe
|
||||||
|
|
||||||
def open_browser(_: Any) -> None:
|
def open_browser(_: Any) -> None:
|
||||||
"""Open the web interface in a browser."""
|
"""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
|
import webbrowser
|
||||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
webbrowser.open(hass.config.api.base_url)
|
||||||
|
|
||||||
run_callback_threadsafe(
|
run_callback_threadsafe(
|
||||||
hass.loop,
|
hass.loop,
|
||||||
@@ -292,7 +298,7 @@ def setup_and_run_hass(config_dir: str,
|
|||||||
EVENT_HOMEASSISTANT_START, open_browser
|
EVENT_HOMEASSISTANT_START, open_browser
|
||||||
)
|
)
|
||||||
|
|
||||||
return hass.start()
|
return await hass.async_run()
|
||||||
|
|
||||||
|
|
||||||
def try_to_restart() -> None:
|
def try_to_restart() -> None:
|
||||||
@@ -347,7 +353,20 @@ def main() -> int:
|
|||||||
monkey_patch.disable_c_asyncio()
|
monkey_patch.disable_c_asyncio()
|
||||||
monkey_patch.patch_weakref_tasks()
|
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()
|
args = get_arguments()
|
||||||
|
|
||||||
@@ -366,11 +385,12 @@ def main() -> int:
|
|||||||
if args.pid_file:
|
if args.pid_file:
|
||||||
write_pid(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:
|
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||||
try_to_restart()
|
try_to_restart()
|
||||||
|
|
||||||
return exit_code
|
return exit_code # type: ignore # mypy cannot yet infer it
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ from . import auth_store, models
|
|||||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||||
|
|
||||||
|
EVENT_USER_ADDED = 'user_added'
|
||||||
|
EVENT_USER_REMOVED = 'user_removed'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
|
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
|
||||||
_ProviderKey = Tuple[str, Optional[str]]
|
_ProviderKey = Tuple[str, Optional[str]]
|
||||||
@@ -126,23 +129,38 @@ class AuthManager:
|
|||||||
|
|
||||||
async def async_create_system_user(self, name: str) -> models.User:
|
async def async_create_system_user(self, name: str) -> models.User:
|
||||||
"""Create a system user."""
|
"""Create a system user."""
|
||||||
return await self._store.async_create_user(
|
user = await self._store.async_create_user(
|
||||||
name=name,
|
name=name,
|
||||||
system_generated=True,
|
system_generated=True,
|
||||||
is_active=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:
|
async def async_create_user(self, name: str) -> models.User:
|
||||||
"""Create a user."""
|
"""Create a user."""
|
||||||
|
group = (await self._store.async_get_groups())[0]
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'is_active': True,
|
'is_active': True,
|
||||||
|
'groups': [group]
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if await self._user_should_be_owner():
|
if await self._user_should_be_owner():
|
||||||
kwargs['is_owner'] = True
|
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) \
|
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
||||||
-> models.User:
|
-> models.User:
|
||||||
@@ -162,12 +180,18 @@ class AuthManager:
|
|||||||
info = await auth_provider.async_user_meta_for_credentials(
|
info = await auth_provider.async_user_meta_for_credentials(
|
||||||
credentials)
|
credentials)
|
||||||
|
|
||||||
return await self._store.async_create_user(
|
user = await self._store.async_create_user(
|
||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
name=info.name,
|
name=info.name,
|
||||||
is_active=info.is_active,
|
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,
|
async def async_link_user(self, user: models.User,
|
||||||
credentials: models.Credentials) -> None:
|
credentials: models.Credentials) -> None:
|
||||||
"""Link credentials to an existing user."""
|
"""Link credentials to an existing user."""
|
||||||
@@ -185,6 +209,10 @@ class AuthManager:
|
|||||||
|
|
||||||
await self._store.async_remove_user(user)
|
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:
|
async def async_activate_user(self, user: models.User) -> None:
|
||||||
"""Activate a user."""
|
"""Activate a user."""
|
||||||
await self._store.async_activate_user(user)
|
await self._store.async_activate_user(user)
|
||||||
@@ -314,7 +342,6 @@ class AuthManager:
|
|||||||
"""Create a new access token."""
|
"""Create a new access token."""
|
||||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
return jwt.encode({
|
return jwt.encode({
|
||||||
'iss': refresh_token.id,
|
'iss': refresh_token.id,
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
"""Storage for auth models."""
|
"""Storage for auth models."""
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import hmac
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Any, Dict, List, Optional # noqa: F401
|
from typing import Any, Dict, List, Optional # noqa: F401
|
||||||
import hmac
|
|
||||||
|
|
||||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
from .permissions import DEFAULT_POLICY
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = 'auth'
|
STORAGE_KEY = 'auth'
|
||||||
|
INITIAL_GROUP_NAME = 'All Access'
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
@@ -28,7 +30,17 @@ class AuthStore:
|
|||||||
"""Initialize the auth store."""
|
"""Initialize the auth store."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._users = None # type: Optional[Dict[str, models.User]]
|
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]:
|
async def async_get_users(self) -> List[models.User]:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
@@ -50,14 +62,20 @@ class AuthStore:
|
|||||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||||
is_active: Optional[bool] = None,
|
is_active: Optional[bool] = None,
|
||||||
system_generated: 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."""
|
"""Create a new user."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
assert self._users is not None
|
|
||||||
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
kwargs = {
|
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]
|
} # type: Dict[str, Any]
|
||||||
|
|
||||||
if is_owner is not None:
|
if is_owner is not None:
|
||||||
@@ -213,14 +231,45 @@ class AuthStore:
|
|||||||
if self._users is not None:
|
if self._users is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
users = OrderedDict() # type: Dict[str, models.User]
|
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
self._users = users
|
self._set_defaults()
|
||||||
return
|
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']:
|
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']:
|
for cred_dict in data['credentials']:
|
||||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||||
@@ -275,6 +324,7 @@ class AuthStore:
|
|||||||
)
|
)
|
||||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||||
|
|
||||||
|
self._groups = groups
|
||||||
self._users = users
|
self._users = users
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -289,10 +339,12 @@ class AuthStore:
|
|||||||
def _data_to_save(self) -> Dict:
|
def _data_to_save(self) -> Dict:
|
||||||
"""Return the data to store."""
|
"""Return the data to store."""
|
||||||
assert self._users is not None
|
assert self._users is not None
|
||||||
|
assert self._groups is not None
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{
|
{
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
|
'group_ids': [group.id for group in user.groups],
|
||||||
'is_owner': user.is_owner,
|
'is_owner': user.is_owner,
|
||||||
'is_active': user.is_active,
|
'is_active': user.is_active,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
@@ -301,6 +353,18 @@ class AuthStore:
|
|||||||
for user in self._users.values()
|
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 = [
|
credentials = [
|
||||||
{
|
{
|
||||||
'id': credential.id,
|
'id': credential.id,
|
||||||
@@ -337,6 +401,22 @@ class AuthStore:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'users': users,
|
'users': users,
|
||||||
|
'groups': groups,
|
||||||
'credentials': credentials,
|
'credentials': credentials,
|
||||||
'refresh_tokens': refresh_tokens,
|
'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
|
from datetime import timedelta
|
||||||
|
|
||||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||||
|
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Plugable auth modules for Home Assistant."""
|
"""Plugable auth modules for Home Assistant."""
|
||||||
from datetime import timedelta
|
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import types
|
import types
|
||||||
@@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_ID): str,
|
vol.Optional(CONF_ID): str,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
SESSION_EXPIRATION = timedelta(minutes=5)
|
|
||||||
|
|
||||||
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -34,6 +31,7 @@ class MultiFactorAuthModule:
|
|||||||
"""Multi-factor Auth Module of validation function."""
|
"""Multi-factor Auth Module of validation function."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Unnamed auth module'
|
DEFAULT_TITLE = 'Unnamed auth module'
|
||||||
|
MAX_RETRY_TIME = 3
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
"""Initialize an auth module."""
|
"""Initialize an auth module."""
|
||||||
@@ -84,7 +82,7 @@ class MultiFactorAuthModule:
|
|||||||
"""Return whether user is setup."""
|
"""Return whether user is setup."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validate(
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -106,7 +104,7 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
|||||||
-> Dict[str, Any]:
|
-> Dict[str, Any]:
|
||||||
"""Handle the first step of setup flow.
|
"""Handle the first step of setup flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
Return self.async_create_entry(data={'result': result}) if finish.
|
Return self.async_create_entry(data={'result': result}) if finish.
|
||||||
"""
|
"""
|
||||||
errors = {} # type: Dict[str, str]
|
errors = {} # type: Dict[str, str]
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validate(
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
for data in self._data:
|
for data in self._data:
|
||||||
|
|||||||
325
homeassistant/auth/mfa_modules/notify.py
Normal file
325
homeassistant/auth/mfa_modules/notify.py
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
"""HMAC-based One-time Password auth module.
|
||||||
|
|
||||||
|
Sending HOTP through notify service
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||||
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyotp==2.2.6']
|
||||||
|
|
||||||
|
CONF_MESSAGE = 'message'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_MESSAGE,
|
||||||
|
default='{} is your Home Assistant login code'): str
|
||||||
|
}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
STORAGE_KEY = 'auth_module.notify'
|
||||||
|
STORAGE_USERS = 'users'
|
||||||
|
STORAGE_USER_ID = 'user_id'
|
||||||
|
|
||||||
|
INPUT_FIELD_CODE = 'code'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_secret() -> str:
|
||||||
|
"""Generate a secret."""
|
||||||
|
import pyotp
|
||||||
|
return str(pyotp.random_base32())
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_random() -> int:
|
||||||
|
"""Generate a 8 digit number."""
|
||||||
|
import pyotp
|
||||||
|
return int(pyotp.random_base32(length=8, chars=list('1234567890')))
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_otp(secret: str, count: int) -> str:
|
||||||
|
"""Generate one time password."""
|
||||||
|
import pyotp
|
||||||
|
return str(pyotp.HOTP(secret).at(count))
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||||
|
"""Verify one time password."""
|
||||||
|
import pyotp
|
||||||
|
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class NotifySetting:
|
||||||
|
"""Store notify setting for one user."""
|
||||||
|
|
||||||
|
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
|
||||||
|
counter = attr.ib(type=int, factory=_generate_random) # not persistent
|
||||||
|
notify_service = attr.ib(type=Optional[str], default=None)
|
||||||
|
target = attr.ib(type=Optional[str], default=None)
|
||||||
|
|
||||||
|
|
||||||
|
_UsersDict = Dict[str, NotifySetting]
|
||||||
|
|
||||||
|
|
||||||
|
@MULTI_FACTOR_AUTH_MODULES.register('notify')
|
||||||
|
class NotifyAuthModule(MultiFactorAuthModule):
|
||||||
|
"""Auth module send hmac-based one time password by notify service."""
|
||||||
|
|
||||||
|
DEFAULT_TITLE = 'Notify One-Time Password'
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
|
"""Initialize the user data store."""
|
||||||
|
super().__init__(hass, config)
|
||||||
|
self._user_settings = None # type: Optional[_UsersDict]
|
||||||
|
self._user_store = hass.helpers.storage.Store(
|
||||||
|
STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||||
|
self._include = config.get(CONF_INCLUDE, [])
|
||||||
|
self._exclude = config.get(CONF_EXCLUDE, [])
|
||||||
|
self._message_template = config[CONF_MESSAGE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_schema(self) -> vol.Schema:
|
||||||
|
"""Validate login flow input data."""
|
||||||
|
return vol.Schema({INPUT_FIELD_CODE: str})
|
||||||
|
|
||||||
|
async def _async_load(self) -> None:
|
||||||
|
"""Load stored data."""
|
||||||
|
data = await self._user_store.async_load()
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
data = {STORAGE_USERS: {}}
|
||||||
|
|
||||||
|
self._user_settings = {
|
||||||
|
user_id: NotifySetting(**setting)
|
||||||
|
for user_id, setting in data.get(STORAGE_USERS, {}).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _async_save(self) -> None:
|
||||||
|
"""Save data."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._user_store.async_save({STORAGE_USERS: {
|
||||||
|
user_id: attr.asdict(
|
||||||
|
notify_setting, filter=attr.filters.exclude(
|
||||||
|
attr.fields(NotifySetting).secret,
|
||||||
|
attr.fields(NotifySetting).counter,
|
||||||
|
))
|
||||||
|
for user_id, notify_setting
|
||||||
|
in self._user_settings.items()
|
||||||
|
}})
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def aync_get_available_notify_services(self) -> List[str]:
|
||||||
|
"""Return list of notify services."""
|
||||||
|
unordered_services = set()
|
||||||
|
|
||||||
|
for service in self.hass.services.async_services().get('notify', {}):
|
||||||
|
if service not in self._exclude:
|
||||||
|
unordered_services.add(service)
|
||||||
|
|
||||||
|
if self._include:
|
||||||
|
unordered_services &= set(self._include)
|
||||||
|
|
||||||
|
return sorted(unordered_services)
|
||||||
|
|
||||||
|
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||||
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
||||||
|
Mfa module should extend SetupFlow
|
||||||
|
"""
|
||||||
|
return NotifySetupFlow(
|
||||||
|
self, self.input_schema, user_id,
|
||||||
|
self.aync_get_available_notify_services())
|
||||||
|
|
||||||
|
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||||
|
"""Set up auth module for user."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
self._user_settings[user_id] = NotifySetting(
|
||||||
|
notify_service=setup_data.get('notify_service'),
|
||||||
|
target=setup_data.get('target'),
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._async_save()
|
||||||
|
|
||||||
|
async def async_depose_user(self, user_id: str) -> None:
|
||||||
|
"""Depose auth module for user."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
if self._user_settings.pop(user_id, None):
|
||||||
|
await self._async_save()
|
||||||
|
|
||||||
|
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||||
|
"""Return whether user is setup."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
return user_id in self._user_settings
|
||||||
|
|
||||||
|
async def async_validate(
|
||||||
|
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
|
"""Return True if validation passed."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
notify_setting = self._user_settings.get(user_id, None)
|
||||||
|
if notify_setting is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# user_input has been validate in caller
|
||||||
|
return await self.hass.async_add_executor_job(
|
||||||
|
_verify_otp, notify_setting.secret,
|
||||||
|
user_input.get(INPUT_FIELD_CODE, ''),
|
||||||
|
notify_setting.counter)
|
||||||
|
|
||||||
|
async def async_initialize_login_mfa_step(self, user_id: str) -> None:
|
||||||
|
"""Generate code and notify user."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
notify_setting = self._user_settings.get(user_id, None)
|
||||||
|
if notify_setting is None:
|
||||||
|
raise ValueError('Cannot find user_id')
|
||||||
|
|
||||||
|
def generate_secret_and_one_time_password() -> str:
|
||||||
|
"""Generate and send one time password."""
|
||||||
|
assert notify_setting
|
||||||
|
# secret and counter are not persistent
|
||||||
|
notify_setting.secret = _generate_secret()
|
||||||
|
notify_setting.counter = _generate_random()
|
||||||
|
return _generate_otp(
|
||||||
|
notify_setting.secret, notify_setting.counter)
|
||||||
|
|
||||||
|
code = await self.hass.async_add_executor_job(
|
||||||
|
generate_secret_and_one_time_password)
|
||||||
|
|
||||||
|
await self.async_notify_user(user_id, code)
|
||||||
|
|
||||||
|
async def async_notify_user(self, user_id: str, code: str) -> None:
|
||||||
|
"""Send code by user's notify service."""
|
||||||
|
if self._user_settings is None:
|
||||||
|
await self._async_load()
|
||||||
|
assert self._user_settings is not None
|
||||||
|
|
||||||
|
notify_setting = self._user_settings.get(user_id, None)
|
||||||
|
if notify_setting is None:
|
||||||
|
_LOGGER.error('Cannot find user %s', user_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.async_notify( # type: ignore
|
||||||
|
code, notify_setting.notify_service, notify_setting.target)
|
||||||
|
|
||||||
|
async def async_notify(self, code: str, notify_service: str,
|
||||||
|
target: Optional[str] = None) -> None:
|
||||||
|
"""Send code by notify service."""
|
||||||
|
data = {'message': self._message_template.format(code)}
|
||||||
|
if target:
|
||||||
|
data['target'] = [target]
|
||||||
|
|
||||||
|
await self.hass.services.async_call('notify', notify_service, data)
|
||||||
|
|
||||||
|
|
||||||
|
class NotifySetupFlow(SetupFlow):
|
||||||
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
|
def __init__(self, auth_module: NotifyAuthModule,
|
||||||
|
setup_schema: vol.Schema,
|
||||||
|
user_id: str,
|
||||||
|
available_notify_services: List[str]) -> None:
|
||||||
|
"""Initialize the setup flow."""
|
||||||
|
super().__init__(auth_module, setup_schema, user_id)
|
||||||
|
# to fix typing complaint
|
||||||
|
self._auth_module = auth_module # type: NotifyAuthModule
|
||||||
|
self._available_notify_services = available_notify_services
|
||||||
|
self._secret = None # type: Optional[str]
|
||||||
|
self._count = None # type: Optional[int]
|
||||||
|
self._notify_service = None # type: Optional[str]
|
||||||
|
self._target = None # type: Optional[str]
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: Optional[Dict[str, str]] = None) \
|
||||||
|
-> Dict[str, Any]:
|
||||||
|
"""Let user select available notify services."""
|
||||||
|
errors = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
hass = self._auth_module.hass
|
||||||
|
if user_input:
|
||||||
|
self._notify_service = user_input['notify_service']
|
||||||
|
self._target = user_input.get('target')
|
||||||
|
self._secret = await hass.async_add_executor_job(_generate_secret)
|
||||||
|
self._count = await hass.async_add_executor_job(_generate_random)
|
||||||
|
|
||||||
|
return await self.async_step_setup()
|
||||||
|
|
||||||
|
if not self._available_notify_services:
|
||||||
|
return self.async_abort(reason='no_available_service')
|
||||||
|
|
||||||
|
schema = OrderedDict() # type: Dict[str, Any]
|
||||||
|
schema['notify_service'] = vol.In(self._available_notify_services)
|
||||||
|
schema['target'] = vol.Optional(str)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='init',
|
||||||
|
data_schema=vol.Schema(schema),
|
||||||
|
errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_setup(
|
||||||
|
self, user_input: Optional[Dict[str, str]] = None) \
|
||||||
|
-> Dict[str, Any]:
|
||||||
|
"""Verify user can recevie one-time password."""
|
||||||
|
errors = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
hass = self._auth_module.hass
|
||||||
|
if user_input:
|
||||||
|
verified = await hass.async_add_executor_job(
|
||||||
|
_verify_otp, self._secret, user_input['code'], self._count)
|
||||||
|
if verified:
|
||||||
|
await self._auth_module.async_setup_user(
|
||||||
|
self._user_id, {
|
||||||
|
'notify_service': self._notify_service,
|
||||||
|
'target': self._target,
|
||||||
|
})
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._auth_module.name,
|
||||||
|
data={}
|
||||||
|
)
|
||||||
|
|
||||||
|
errors['base'] = 'invalid_code'
|
||||||
|
|
||||||
|
# generate code every time, no retry logic
|
||||||
|
assert self._secret and self._count
|
||||||
|
code = await hass.async_add_executor_job(
|
||||||
|
_generate_otp, self._secret, self._count)
|
||||||
|
|
||||||
|
assert self._notify_service
|
||||||
|
await self._auth_module.async_notify(
|
||||||
|
code, self._notify_service, self._target)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='setup',
|
||||||
|
data_schema=self._setup_schema,
|
||||||
|
description_placeholders={'notify_service': self._notify_service},
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
@@ -60,13 +60,14 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
"""Auth module validate time-based one time password."""
|
"""Auth module validate time-based one time password."""
|
||||||
|
|
||||||
DEFAULT_TITLE = '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:
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
super().__init__(hass, config)
|
super().__init__(hass, config)
|
||||||
self._users = None # type: Optional[Dict[str, str]]
|
self._users = None # type: Optional[Dict[str, str]]
|
||||||
self._user_store = hass.helpers.storage.Store(
|
self._user_store = hass.helpers.storage.Store(
|
||||||
STORAGE_VERSION, STORAGE_KEY)
|
STORAGE_VERSION, STORAGE_KEY, private=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_schema(self) -> vol.Schema:
|
def input_schema(self) -> vol.Schema:
|
||||||
@@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
return user_id in self._users # type: ignore
|
return user_id in self._users # type: ignore
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validate(
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
@@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
if ota_secret is None:
|
if ota_secret is None:
|
||||||
# even we cannot find user, we still do verify
|
# even we cannot find user, we still do verify
|
||||||
# to make timing the same as if user was found.
|
# to make timing the same as if user was found.
|
||||||
pyotp.TOTP(DUMMY_SECRET).verify(code)
|
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return bool(pyotp.TOTP(ota_secret).verify(code))
|
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
|
||||||
|
|
||||||
|
|
||||||
class TotpSetupFlow(SetupFlow):
|
class TotpSetupFlow(SetupFlow):
|
||||||
@@ -175,7 +176,7 @@ class TotpSetupFlow(SetupFlow):
|
|||||||
-> Dict[str, Any]:
|
-> Dict[str, Any]:
|
||||||
"""Handle the first step of setup flow.
|
"""Handle the first step of setup flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
Return self.async_create_entry(data={'result': result}) if finish.
|
Return self.async_create_entry(data={'result': result}) if finish.
|
||||||
"""
|
"""
|
||||||
import pyotp
|
import pyotp
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import attr
|
|||||||
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from . import permissions as perm_mdl
|
||||||
from .util import generate_secret
|
from .util import generate_secret
|
||||||
|
|
||||||
TOKEN_TYPE_NORMAL = 'normal'
|
TOKEN_TYPE_NORMAL = 'normal'
|
||||||
@@ -14,26 +15,59 @@ TOKEN_TYPE_SYSTEM = 'system'
|
|||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
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)
|
@attr.s(slots=True)
|
||||||
class User:
|
class User:
|
||||||
"""A user."""
|
"""A user."""
|
||||||
|
|
||||||
name = attr.ib(type=str) # type: Optional[str]
|
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_owner = attr.ib(type=bool, default=False)
|
||||||
is_active = attr.ib(type=bool, default=False)
|
is_active = attr.ib(type=bool, default=False)
|
||||||
system_generated = 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.
|
# List of credentials of a user.
|
||||||
credentials = attr.ib(
|
credentials = attr.ib(
|
||||||
type=list, default=attr.Factory(list), cmp=False
|
type=list, factory=list, cmp=False
|
||||||
) # type: List[Credentials]
|
) # type: List[Credentials]
|
||||||
|
|
||||||
# Tokens associated with a user.
|
# Tokens associated with a user.
|
||||||
refresh_tokens = attr.ib(
|
refresh_tokens = attr.ib(
|
||||||
type=dict, default=attr.Factory(dict), cmp=False
|
type=dict, factory=dict, cmp=False
|
||||||
) # type: Dict[str, RefreshToken]
|
) # 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)
|
@attr.s(slots=True)
|
||||||
class RefreshToken:
|
class RefreshToken:
|
||||||
@@ -48,12 +82,10 @@ class RefreshToken:
|
|||||||
validator=attr.validators.in_((
|
validator=attr.validators.in_((
|
||||||
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
|
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
|
||||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
created_at = attr.ib(type=datetime, factory=dt_util.utcnow)
|
||||||
token = attr.ib(type=str,
|
token = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||||
default=attr.Factory(lambda: generate_secret(64)))
|
jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||||
jwt_key = attr.ib(type=str,
|
|
||||||
default=attr.Factory(lambda: generate_secret(64)))
|
|
||||||
|
|
||||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
||||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
last_used_ip = attr.ib(type=Optional[str], default=None)
|
||||||
@@ -69,7 +101,7 @@ class Credentials:
|
|||||||
# Allow the auth provider to store data to represent their auth.
|
# Allow the auth provider to store data to represent their auth.
|
||||||
data = attr.ib(type=dict)
|
data = 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)
|
is_new = attr.ib(type=bool, default=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
97
homeassistant/auth/permissions/__init__.py
Normal file
97
homeassistant/auth/permissions/__init__.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""Permissions for Home Assistant."""
|
||||||
|
import logging
|
||||||
|
from typing import ( # noqa: F401
|
||||||
|
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import State
|
||||||
|
|
||||||
|
from .common import CategoryType, PolicyType
|
||||||
|
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||||
|
from .merge import merge_policies # noqa
|
||||||
|
|
||||||
|
|
||||||
|
# Default policy if group has no policy applied.
|
||||||
|
DEFAULT_POLICY = {
|
||||||
|
"entities": True
|
||||||
|
} # type: PolicyType
|
||||||
|
|
||||||
|
CAT_ENTITIES = 'entities'
|
||||||
|
|
||||||
|
POLICY_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||||
|
})
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractPermissions:
|
||||||
|
"""Default permissions class."""
|
||||||
|
|
||||||
|
def check_entity(self, entity_id: str, key: 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, key: str) -> bool:
|
||||||
|
"""Test if we can access entity."""
|
||||||
|
func = self._policy_func(CAT_ENTITIES, compile_entities)
|
||||||
|
return func(entity_id, (key,))
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
_LOGGER.debug("Compiled %s func: %s", category, func)
|
||||||
|
|
||||||
|
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, key: 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
|
||||||
33
homeassistant/auth/permissions/common.py
Normal file
33
homeassistant/auth/permissions/common.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Common code for permissions."""
|
||||||
|
from typing import ( # noqa: F401
|
||||||
|
Mapping, Union, Any)
|
||||||
|
|
||||||
|
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||||
|
|
||||||
|
ValueType = Union[
|
||||||
|
# Example: entities.all = { read: true, control: true }
|
||||||
|
Mapping[str, bool],
|
||||||
|
bool,
|
||||||
|
None
|
||||||
|
]
|
||||||
|
|
||||||
|
SubCategoryType = Union[
|
||||||
|
# Example: entities.domains = { light: … }
|
||||||
|
Mapping[str, ValueType],
|
||||||
|
bool,
|
||||||
|
None
|
||||||
|
]
|
||||||
|
|
||||||
|
CategoryType = Union[
|
||||||
|
# Example: entities.domains
|
||||||
|
Mapping[str, SubCategoryType],
|
||||||
|
# Example: entities.all
|
||||||
|
Mapping[str, ValueType],
|
||||||
|
bool,
|
||||||
|
None
|
||||||
|
]
|
||||||
|
|
||||||
|
# Example: { entities: … }
|
||||||
|
PolicyType = Mapping[str, CategoryType]
|
||||||
|
|
||||||
|
SUBCAT_ALL = 'all'
|
||||||
149
homeassistant/auth/permissions/entities.py
Normal file
149
homeassistant/auth/permissions/entities.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"""Entity permissions."""
|
||||||
|
from functools import wraps
|
||||||
|
from typing import ( # noqa: F401
|
||||||
|
Callable, Dict, List, Tuple, Union)
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from .common import CategoryType, ValueType, SUBCAT_ALL
|
||||||
|
|
||||||
|
|
||||||
|
POLICY_READ = 'read'
|
||||||
|
POLICY_CONTROL = 'control'
|
||||||
|
POLICY_EDIT = 'edit'
|
||||||
|
|
||||||
|
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
||||||
|
vol.Optional(POLICY_READ): True,
|
||||||
|
vol.Optional(POLICY_CONTROL): True,
|
||||||
|
vol.Optional(POLICY_EDIT): True,
|
||||||
|
}))
|
||||||
|
|
||||||
|
ENTITY_DOMAINS = 'domains'
|
||||||
|
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||||
|
|
||||||
|
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||||
|
str: SINGLE_ENTITY_SCHEMA
|
||||||
|
}))
|
||||||
|
|
||||||
|
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||||
|
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
|
||||||
|
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
|
||||||
|
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
def _entity_allowed(schema: ValueType, keys: Tuple[str]) \
|
||||||
|
-> Union[bool, None]:
|
||||||
|
"""Test if an entity is allowed based on the keys."""
|
||||||
|
if schema is None or isinstance(schema, bool):
|
||||||
|
return schema
|
||||||
|
assert isinstance(schema, dict)
|
||||||
|
return schema.get(keys[0])
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
all_entities = policy.get(SUBCAT_ALL)
|
||||||
|
|
||||||
|
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 allowed_entity_id_bool(entity_id: str, keys: Tuple[str]) -> bool:
|
||||||
|
"""Test if allowed entity_id."""
|
||||||
|
return entity_ids # type: ignore
|
||||||
|
|
||||||
|
return allowed_entity_id_bool
|
||||||
|
|
||||||
|
if entity_ids is not None:
|
||||||
|
def allowed_entity_id_dict(entity_id: str, keys: Tuple[str]) \
|
||||||
|
-> Union[None, bool]:
|
||||||
|
"""Test if allowed entity_id."""
|
||||||
|
return _entity_allowed(
|
||||||
|
entity_ids.get(entity_id), keys) # type: ignore
|
||||||
|
|
||||||
|
funcs.append(allowed_entity_id_dict)
|
||||||
|
|
||||||
|
if isinstance(domains, bool):
|
||||||
|
def allowed_domain_bool(entity_id: str, keys: Tuple[str]) \
|
||||||
|
-> Union[None, bool]:
|
||||||
|
"""Test if allowed domain."""
|
||||||
|
return domains
|
||||||
|
|
||||||
|
funcs.append(allowed_domain_bool)
|
||||||
|
|
||||||
|
elif domains is not None:
|
||||||
|
def allowed_domain_dict(entity_id: str, keys: Tuple[str]) \
|
||||||
|
-> Union[None, bool]:
|
||||||
|
"""Test if allowed domain."""
|
||||||
|
domain = entity_id.split(".", 1)[0]
|
||||||
|
return _entity_allowed(domains.get(domain), keys) # type: ignore
|
||||||
|
|
||||||
|
funcs.append(allowed_domain_dict)
|
||||||
|
|
||||||
|
if isinstance(all_entities, bool):
|
||||||
|
def allowed_all_entities_bool(entity_id: str, keys: Tuple[str]) \
|
||||||
|
-> Union[None, bool]:
|
||||||
|
"""Test if allowed domain."""
|
||||||
|
return all_entities
|
||||||
|
funcs.append(allowed_all_entities_bool)
|
||||||
|
|
||||||
|
elif all_entities is not None:
|
||||||
|
def allowed_all_entities_dict(entity_id: str, keys: Tuple[str]) \
|
||||||
|
-> Union[None, bool]:
|
||||||
|
"""Test if allowed domain."""
|
||||||
|
return _entity_allowed(all_entities, keys)
|
||||||
|
funcs.append(allowed_all_entities_dict)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
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
|
||||||
65
homeassistant/auth/permissions/merge.py
Normal file
65
homeassistant/auth/permissions/merge.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""Merging of policies."""
|
||||||
|
from typing import ( # noqa: F401
|
||||||
|
cast, Dict, List, Set)
|
||||||
|
|
||||||
|
from .common import PolicyType, CategoryType
|
||||||
|
|
||||||
|
|
||||||
|
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 = cast(CategoryType, {})
|
||||||
|
|
||||||
|
assert isinstance(policy, dict)
|
||||||
|
|
||||||
|
for key in source:
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
key_sources = []
|
||||||
|
for src in sources:
|
||||||
|
if isinstance(src, dict):
|
||||||
|
key_sources.append(src.get(key))
|
||||||
|
|
||||||
|
policy[key] = _merge_policies(key_sources)
|
||||||
|
|
||||||
|
return policy
|
||||||
@@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util
|
|||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
|
|
||||||
from ..auth_store import AuthStore
|
from ..auth_store import AuthStore
|
||||||
|
from ..const import MFA_SESSION_EXPIRATION
|
||||||
from ..models import Credentials, User, UserMeta # noqa: F401
|
from ..models import Credentials, User, UserMeta # noqa: F401
|
||||||
from ..mfa_modules import SESSION_EXPIRATION
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DATA_REQS = 'auth_prov_reqs_processed'
|
DATA_REQS = 'auth_prov_reqs_processed'
|
||||||
@@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
self._auth_manager = auth_provider.hass.auth # type: ignore
|
||||||
self.available_mfa_modules = {} # type: Dict[str, str]
|
self.available_mfa_modules = {} # type: Dict[str, str]
|
||||||
self.created_at = dt_util.utcnow()
|
self.created_at = dt_util.utcnow()
|
||||||
|
self.invalid_mfa_times = 0
|
||||||
self.user = None # type: Optional[User]
|
self.user = None # type: Optional[User]
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@@ -178,7 +179,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
-> Dict[str, Any]:
|
-> Dict[str, Any]:
|
||||||
"""Handle the first step of login flow.
|
"""Handle the first step of login flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
Return await self.async_finish(flow_result) if login init step pass.
|
Return await self.async_finish(flow_result) if login init step pass.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None) \
|
||||||
-> Dict[str, Any]:
|
-> Dict[str, Any]:
|
||||||
"""Handle the step of mfa validation."""
|
"""Handle the step of mfa validation."""
|
||||||
|
assert self.user
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
auth_module = self._auth_manager.get_auth_mfa_module(
|
auth_module = self._auth_manager.get_auth_mfa_module(
|
||||||
@@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||||||
# will show invalid_auth_module error
|
# will show invalid_auth_module error
|
||||||
return await self.async_step_select_mfa_module(user_input={})
|
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:
|
if user_input is not None:
|
||||||
expires = self.created_at + SESSION_EXPIRATION
|
expires = self.created_at + MFA_SESSION_EXPIRATION
|
||||||
if dt_util.utcnow() > expires:
|
if dt_util.utcnow() > expires:
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
reason='login_expired'
|
reason='login_expired'
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await auth_module.async_validation(
|
result = await auth_module.async_validate(
|
||||||
self.user.id, user_input) # type: ignore
|
self.user.id, user_input)
|
||||||
if not result:
|
if not result:
|
||||||
errors['base'] = 'invalid_code'
|
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:
|
if not errors:
|
||||||
return await self.async_finish(self.user)
|
return await self.async_finish(self.user)
|
||||||
|
|
||||||
description_placeholders = {
|
description_placeholders = {
|
||||||
'mfa_module_name': auth_module.name,
|
'mfa_module_name': auth_module.name,
|
||||||
'mfa_module_id': auth_module.id
|
'mfa_module_id': auth_module.id,
|
||||||
} # type: Dict[str, str]
|
} # type: Dict[str, Optional[str]]
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='mfa',
|
step_id='mfa',
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ class Data:
|
|||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
self.hass = hass
|
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]]
|
self._data = None # type: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
async def async_load(self) -> None:
|
async def async_load(self) -> None:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from time import time
|
from time import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Optional, Dict
|
||||||
|
|
||||||
import voluptuous as vol
|
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.package import async_get_user_site, is_virtual_env
|
||||||
from homeassistant.util.yaml import clear_secret_cache
|
from homeassistant.util.yaml import clear_secret_cache
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.signal import async_register_signal_handling
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||||||
stop = time()
|
stop = time()
|
||||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||||
|
|
||||||
async_register_signal_handling(hass)
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import itertools as it
|
|||||||
import logging
|
import logging
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.config as conf_util
|
import homeassistant.config as conf_util
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@@ -21,11 +23,16 @@ from homeassistant.const import (
|
|||||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||||
RESTART_EXIT_CODE)
|
RESTART_EXIT_CODE)
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||||
SERVICE_CHECK_CONFIG = 'check_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):
|
def is_on(hass, entity_id=None):
|
||||||
@@ -59,61 +66,9 @@ def is_on(hass, entity_id=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def turn_on(hass, entity_id=None, **service_data):
|
async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
"""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]:
|
|
||||||
"""Set up general services related to Home Assistant."""
|
"""Set up general services related to Home Assistant."""
|
||||||
@asyncio.coroutine
|
async def async_handle_turn_service(service):
|
||||||
def async_handle_turn_service(service):
|
|
||||||
"""Handle calls to homeassistant.turn_on/off."""
|
"""Handle calls to homeassistant.turn_on/off."""
|
||||||
entity_ids = extract_entity_ids(hass, service)
|
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(
|
tasks.append(hass.services.async_call(
|
||||||
domain, service.service, data, blocking))
|
domain, service.service, data, blocking))
|
||||||
|
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
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(
|
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_handle_core_service(call):
|
||||||
def async_handle_core_service(call):
|
|
||||||
"""Service handler for handling core services."""
|
"""Service handler for handling core services."""
|
||||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||||
hass.async_create_task(hass.async_stop())
|
hass.async_create_task(hass.async_stop())
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
errors = yield from conf_util.async_check_ha_config_file(hass)
|
errors = await conf_util.async_check_ha_config_file(hass)
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -186,23 +140,33 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
|||||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
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(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
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
|
async def async_handle_reload_config(call):
|
||||||
def async_handle_reload_config(call):
|
|
||||||
"""Service handler for reloading core config."""
|
"""Service handler for reloading core config."""
|
||||||
try:
|
try:
|
||||||
conf = yield from conf_util.async_hass_config_yaml(hass)
|
conf = await conf_util.async_hass_config_yaml(hass)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
return
|
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, conf.get(ha.DOMAIN) or {})
|
||||||
|
|
||||||
hass.services.async_register(
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/abode/
|
https://home-assistant.io/components/abode/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from requests.exceptions import HTTPError, ConnectTimeout
|
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 import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['abodepy==0.13.1']
|
REQUIREMENTS = ['abodepy==0.14.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -261,8 +260,7 @@ class AbodeDevice(Entity):
|
|||||||
self._data = data
|
self._data = data
|
||||||
self._device = device
|
self._device = device
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Subscribe Abode events."""
|
"""Subscribe Abode events."""
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
self._data.abode.events.add_device_callback,
|
self._data.abode.events.add_device_callback,
|
||||||
@@ -308,8 +306,7 @@ class AbodeAutomation(Entity):
|
|||||||
self._automation = automation
|
self._automation = automation
|
||||||
self._event = event
|
self._event = event
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Subscribe Abode events."""
|
"""Subscribe Abode events."""
|
||||||
if self._event:
|
if self._event:
|
||||||
self.hass.async_add_job(
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel/
|
https://home-assistant.io/components/alarm_control_panel/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ from homeassistant.const import (
|
|||||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||||
from homeassistant.loader import bind_hass
|
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
@@ -32,85 +30,12 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
async def async_setup(hass, config):
|
||||||
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):
|
|
||||||
"""Track states and offer events for sensors."""
|
"""Track states and offer events for sensors."""
|
||||||
component = hass.data[DOMAIN] = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
await component.async_setup(config)
|
||||||
|
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -59,8 +58,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
|||||||
self._ready = None
|
self._ready = None
|
||||||
self._zone_bypassed = None
|
self._zone_bypassed = None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up a Alarm.com control panel."""
|
"""Set up a Alarm.com control panel."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
code = config.get(CONF_CODE)
|
code = config.get(CONF_CODE)
|
||||||
@@ -42,7 +40,7 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||||
yield from alarmdotcom.async_login()
|
await alarmdotcom.async_login()
|
||||||
async_add_entities([alarmdotcom])
|
async_add_entities([alarmdotcom])
|
||||||
|
|
||||||
|
|
||||||
@@ -63,15 +61,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
self._alarm = Alarmdotcom(
|
self._alarm = Alarmdotcom(
|
||||||
username, password, self._websession, hass.loop)
|
username, password, self._websession, hass.loop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_login(self):
|
||||||
def async_login(self):
|
|
||||||
"""Login to Alarm.com."""
|
"""Login to Alarm.com."""
|
||||||
yield from self._alarm.async_login()
|
await self._alarm.async_login()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_update(self):
|
||||||
def async_update(self):
|
|
||||||
"""Fetch the latest state."""
|
"""Fetch the latest state."""
|
||||||
yield from self._alarm.async_update()
|
await self._alarm.async_update()
|
||||||
return self._alarm.state
|
return self._alarm.state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -106,23 +102,20 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
'sensor_status': self._alarm.sensor_status
|
'sensor_status': self._alarm.sensor_status
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_disarm(self, code=None):
|
||||||
def async_alarm_disarm(self, code=None):
|
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if self._validate_code(code):
|
if self._validate_code(code):
|
||||||
yield from self._alarm.async_alarm_disarm()
|
await self._alarm.async_alarm_disarm()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_home(self, code=None):
|
||||||
def async_alarm_arm_home(self, code=None):
|
|
||||||
"""Send arm hom command."""
|
"""Send arm hom command."""
|
||||||
if self._validate_code(code):
|
if self._validate_code(code):
|
||||||
yield from self._alarm.async_alarm_arm_home()
|
await self._alarm.async_alarm_arm_home()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_away(self, code=None):
|
||||||
def async_alarm_arm_away(self, code=None):
|
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if self._validate_code(code):
|
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):
|
def _validate_code(self, code):
|
||||||
"""Validate given 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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.egardia/
|
https://home-assistant.io/components/alarm_control_panel.egardia/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -61,8 +60,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||||||
self._rs_codes = rs_codes
|
self._rs_codes = rs_codes
|
||||||
self._rs_port = rs_port
|
self._rs_port = rs_port
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Add Egardiaserver callback if enabled."""
|
"""Add Egardiaserver callback if enabled."""
|
||||||
if self._rs_enabled:
|
if self._rs_enabled:
|
||||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
_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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -32,9 +31,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Perform the setup for Envisalink alarm panels."""
|
"""Perform the setup for Envisalink alarm panels."""
|
||||||
configured_partitions = discovery_info['partitions']
|
configured_partitions = discovery_info['partitions']
|
||||||
code = discovery_info[CONF_CODE]
|
code = discovery_info[CONF_CODE]
|
||||||
@@ -88,8 +86,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||||
super().__init__(alarm_name, info, controller)
|
super().__init__(alarm_name, info, controller)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||||
@@ -128,8 +125,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
state = STATE_ALARM_DISARMED
|
state = STATE_ALARM_DISARMED
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_disarm(self, code=None):
|
||||||
def async_alarm_disarm(self, code=None):
|
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].disarm_partition(
|
self.hass.data[DATA_EVL].disarm_partition(
|
||||||
@@ -138,8 +134,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
self.hass.data[DATA_EVL].disarm_partition(
|
self.hass.data[DATA_EVL].disarm_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_home(self, code=None):
|
||||||
def async_alarm_arm_home(self, code=None):
|
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
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(
|
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_away(self, code=None):
|
||||||
def async_alarm_arm_away(self, code=None):
|
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].arm_away_partition(
|
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(
|
self.hass.data[DATA_EVL].arm_away_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_trigger(self, code=None):
|
||||||
def async_alarm_trigger(self, code=None):
|
|
||||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||||
self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
|
self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import track_point_in_time
|
from homeassistant.helpers.event import track_point_in_time
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.helpers.restore_state import async_get_last_state
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -306,3 +307,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||||
|
|
||||||
return state_attr
|
return state_attr
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Run when entity about to be added to hass."""
|
||||||
|
state = await async_get_last_state(self.hass, self.entity_id)
|
||||||
|
if state:
|
||||||
|
self._state = state.state
|
||||||
|
self._state_ts = state.last_updated
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Support for manual alarms controllable via MQTT.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
@@ -363,8 +362,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||||||
return mqtt.async_subscribe(
|
return mqtt.async_subscribe(
|
||||||
self.hass, self._command_topic, message_received, self._qos)
|
self.hass, self._command_topic, message_received, self._qos)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
new_state):
|
||||||
"""Publish state change to MQTT."""
|
"""Publish state change to MQTT."""
|
||||||
mqtt.async_publish(
|
mqtt.async_publish(
|
||||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -18,10 +17,13 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||||
CONF_NAME, CONF_CODE)
|
CONF_NAME, CONF_CODE)
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||||
CONF_RETAIN, MqttAvailability)
|
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
|
||||||
|
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||||
import homeassistant.helpers.config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
|||||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
"""Set up MQTT alarm control panel through configuration.yaml."""
|
||||||
"""Set up the MQTT Alarm Control Panel platform."""
|
await _async_setup_entity(hass, config, async_add_entities)
|
||||||
if discovery_info is not None:
|
|
||||||
config = PLATFORM_SCHEMA(discovery_info)
|
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
async_add_entities([MqttAlarm(
|
||||||
config.get(CONF_NAME),
|
config.get(CONF_NAME),
|
||||||
config.get(CONF_STATE_TOPIC),
|
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_CODE),
|
||||||
config.get(CONF_AVAILABILITY_TOPIC),
|
config.get(CONF_AVAILABILITY_TOPIC),
|
||||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
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."""
|
"""Representation of a MQTT alarm status."""
|
||||||
|
|
||||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
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."""
|
"""Init the MQTT Alarm Control Panel."""
|
||||||
super().__init__(availability_topic, qos, payload_available,
|
MqttAvailability.__init__(self, availability_topic, qos,
|
||||||
payload_not_available)
|
payload_available, payload_not_available)
|
||||||
|
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
@@ -87,11 +108,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||||||
self._payload_arm_home = payload_arm_home
|
self._payload_arm_home = payload_arm_home
|
||||||
self._payload_arm_away = payload_arm_away
|
self._payload_arm_away = payload_arm_away
|
||||||
self._code = code
|
self._code = code
|
||||||
|
self._discovery_hash = discovery_hash
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Subscribe mqtt events."""
|
"""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
|
@callback
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
@@ -104,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||||||
self._state = payload
|
self._state = payload
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
yield from mqtt.async_subscribe(
|
await mqtt.async_subscribe(
|
||||||
self.hass, self._state_topic, message_received, self._qos)
|
self.hass, self._state_topic, message_received, self._qos)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -131,8 +153,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||||||
return 'Number'
|
return 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_disarm(self, code=None):
|
||||||
def async_alarm_disarm(self, code=None):
|
|
||||||
"""Send disarm command.
|
"""Send disarm command.
|
||||||
|
|
||||||
This method is a coroutine.
|
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.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||||
self._retain)
|
self._retain)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_home(self, code=None):
|
||||||
def async_alarm_arm_home(self, code=None):
|
|
||||||
"""Send arm home command.
|
"""Send arm home command.
|
||||||
|
|
||||||
This method is a coroutine.
|
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.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||||
self._retain)
|
self._retain)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_away(self, code=None):
|
||||||
def async_alarm_arm_away(self, code=None):
|
|
||||||
"""Send arm away command.
|
"""Send arm away command.
|
||||||
|
|
||||||
This method is a coroutine.
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.satel_integra/
|
https://home-assistant.io/components/alarm_control_panel.satel_integra/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
@@ -18,9 +17,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEPENDENCIES = ['satel_integra']
|
DEPENDENCIES = ['satel_integra']
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up for Satel Integra alarm panels."""
|
"""Set up for Satel Integra alarm panels."""
|
||||||
if not discovery_info:
|
if not discovery_info:
|
||||||
return
|
return
|
||||||
@@ -39,8 +37,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
|||||||
self._state = None
|
self._state = None
|
||||||
self._arm_home_mode = arm_home_mode
|
self._arm_home_mode = arm_home_mode
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||||
@@ -74,21 +71,18 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
|||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_disarm(self, code=None):
|
||||||
def async_alarm_disarm(self, code=None):
|
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if code:
|
if code:
|
||||||
yield from self.hass.data[DATA_SATEL].disarm(code)
|
await self.hass.data[DATA_SATEL].disarm(code)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_away(self, code=None):
|
||||||
def async_alarm_arm_away(self, code=None):
|
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if code:
|
if code:
|
||||||
yield from self.hass.data[DATA_SATEL].arm(code)
|
await self.hass.data[DATA_SATEL].arm(code)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_home(self, code=None):
|
||||||
def async_alarm_arm_home(self, code=None):
|
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if code:
|
if code:
|
||||||
yield from self.hass.data[DATA_SATEL].arm(
|
await self.hass.data[DATA_SATEL].arm(
|
||||||
code, self._arm_home_mode)
|
code, self._arm_home_mode)
|
||||||
|
|||||||
@@ -79,3 +79,55 @@ ifttt_push_alarm_state:
|
|||||||
state:
|
state:
|
||||||
description: The state to which the alarm control panel has to be set.
|
description: The state to which the alarm control panel has to be set.
|
||||||
example: 'armed_night'
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
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 logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import voluptuous as vol
|
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||||
|
from homeassistant.components.simplisafe.const import (
|
||||||
from homeassistant.components.alarm_control_panel import (
|
DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
|
||||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_DISARMED)
|
||||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'SimpliSafe'
|
ATTR_ALARM_ACTIVE = 'alarm_active'
|
||||||
|
ATTR_TEMPERATURE = 'temperature'
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up the SimpliSafe platform."""
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
"""Set up a SimpliSafe alarm control panel based on existing config."""
|
||||||
name = config.get(CONF_NAME)
|
pass
|
||||||
code = config.get(CONF_CODE)
|
|
||||||
username = config.get(CONF_USERNAME)
|
|
||||||
password = config.get(CONF_PASSWORD)
|
|
||||||
|
|
||||||
try:
|
|
||||||
simplisafe = SimpliSafeApiInterface(username, password)
|
|
||||||
except SimpliSafeAPIException:
|
|
||||||
_LOGGER.error("Failed to set up SimpliSafe")
|
|
||||||
return
|
|
||||||
|
|
||||||
systems = []
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||||
for system in simplisafe.get_systems():
|
systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||||
systems.append(SimpliSafeAlarm(system, name, code))
|
async_add_entities([
|
||||||
|
SimpliSafeAlarm(system, entry.data.get(CONF_CODE))
|
||||||
add_entities(systems)
|
for system in systems
|
||||||
|
], True)
|
||||||
|
|
||||||
|
|
||||||
class SimpliSafeAlarm(AlarmControlPanel):
|
class SimpliSafeAlarm(AlarmControlPanel):
|
||||||
"""Representation of a SimpliSafe alarm."""
|
"""Representation of a SimpliSafe alarm."""
|
||||||
|
|
||||||
def __init__(self, simplisafe, name, code):
|
def __init__(self, system, code):
|
||||||
"""Initialize the SimpliSafe alarm."""
|
"""Initialize the SimpliSafe alarm."""
|
||||||
self.simplisafe = simplisafe
|
self._async_unsub_dispatcher_connect = None
|
||||||
self._name = name
|
self._attrs = {}
|
||||||
self._code = str(code) if code else None
|
self._code = code
|
||||||
|
self._system = system
|
||||||
|
self._state = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the unique ID."""
|
"""Return the unique ID."""
|
||||||
return self.simplisafe.location_id
|
return self._system.system_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
if self._name is not None:
|
return self._system.address
|
||||||
return self._name
|
|
||||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if not self._code:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||||
return 'Number'
|
return 'Number'
|
||||||
@@ -89,53 +70,12 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
status = self.simplisafe.state
|
return self._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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
attributes = {}
|
return self._attrs
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
@@ -143,3 +83,63 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||||||
if not check:
|
if not check:
|
||||||
_LOGGER.warning("Wrong code entered for %s", state)
|
_LOGGER.warning("Wrong code entered for %s", state)
|
||||||
return check
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.spc/
|
https://home-assistant.io/components/alarm_control_panel.spc/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.spc import (
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||||
STATE_UNKNOWN)
|
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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(area):
|
||||||
def _get_alarm_state(spc_mode):
|
|
||||||
"""Get the alarm state."""
|
"""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
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the SPC alarm control panel platform."""
|
"""Set up the SPC alarm control panel platform."""
|
||||||
if (discovery_info is None or
|
if discovery_info is None:
|
||||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
api = hass.data[DATA_API]
|
api = hass.data[DATA_API]
|
||||||
devices = [SpcAlarm(api, area)
|
async_add_entities([SpcAlarm(area=area, api=api)
|
||||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
for area in api.areas.values()])
|
||||||
|
|
||||||
async_add_entities(devices)
|
|
||||||
|
|
||||||
|
|
||||||
class SpcAlarm(alarm.AlarmControlPanel):
|
class SpcAlarm(alarm.AlarmControlPanel):
|
||||||
"""Representation of the SPC alarm panel."""
|
"""Representation of the SPC alarm panel."""
|
||||||
|
|
||||||
def __init__(self, api, area):
|
def __init__(self, area, api):
|
||||||
"""Initialize the SPC alarm panel."""
|
"""Initialize the SPC alarm panel."""
|
||||||
self._area_id = area['id']
|
self._area = area
|
||||||
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._api = api
|
self._api = api
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Call for adding new entities."""
|
"""Call for adding new entities."""
|
||||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
async_dispatcher_connect(self.hass,
|
||||||
self._area_id, self)
|
SIGNAL_UPDATE_ALARM.format(self._area.id),
|
||||||
|
self._update_callback)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@callback
|
||||||
def async_update_from_spc(self, state, extra):
|
def _update_callback(self):
|
||||||
"""Update the alarm panel with a new state."""
|
"""Call update method."""
|
||||||
self._state = state
|
self.async_schedule_update_ha_state(True)
|
||||||
self._changed_by = extra.get('changed_by', 'unknown')
|
|
||||||
self.async_schedule_update_ha_state()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@@ -78,32 +70,38 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return self._name
|
return self._area.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_by(self):
|
def changed_by(self):
|
||||||
"""Return the user the last change was triggered by."""
|
"""Return the user the last change was triggered by."""
|
||||||
return self._changed_by
|
return self._area.last_changed_by
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._state
|
return _get_alarm_state(self._area)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_disarm(self, code=None):
|
||||||
def async_alarm_disarm(self, code=None):
|
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
yield from self._api.send_area_command(
|
from pyspcwebgw.const import AreaMode
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
await self._api.change_mode(area=self._area,
|
||||||
|
new_mode=AreaMode.UNSET)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_home(self, code=None):
|
||||||
def async_alarm_arm_home(self, code=None):
|
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
yield from self._api.send_area_command(
|
from pyspcwebgw.const import AreaMode
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
await self._api.change_mode(area=self._area,
|
||||||
|
new_mode=AreaMode.PART_SET_A)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_alarm_arm_night(self, code=None):
|
||||||
def async_alarm_arm_away(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."""
|
"""Send arm away command."""
|
||||||
yield from self._api.send_area_command(
|
from pyspcwebgw.const import AreaMode
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
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)
|
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['total_connect_client==0.18']
|
REQUIREMENTS = ['total_connect_client==0.20']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Interfaces with Wink Cameras.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.wink/
|
https://home-assistant.io/components/alarm_control_panel.wink/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
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):
|
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||||
"""Representation a Wink camera alarm."""
|
"""Representation a Wink camera alarm."""
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.components.notify import (
|
||||||
|
ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||||
@@ -23,23 +24,25 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DOMAIN = 'alert'
|
DOMAIN = 'alert'
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
CONF_DONE_MESSAGE = 'done_message'
|
|
||||||
CONF_CAN_ACK = 'can_acknowledge'
|
CONF_CAN_ACK = 'can_acknowledge'
|
||||||
CONF_NOTIFIERS = 'notifiers'
|
CONF_NOTIFIERS = 'notifiers'
|
||||||
CONF_REPEAT = 'repeat'
|
CONF_REPEAT = 'repeat'
|
||||||
CONF_SKIP_FIRST = 'skip_first'
|
CONF_SKIP_FIRST = 'skip_first'
|
||||||
|
CONF_ALERT_MESSAGE = 'message'
|
||||||
|
CONF_DONE_MESSAGE = 'done_message'
|
||||||
|
|
||||||
DEFAULT_CAN_ACK = True
|
DEFAULT_CAN_ACK = True
|
||||||
DEFAULT_SKIP_FIRST = False
|
DEFAULT_SKIP_FIRST = False
|
||||||
|
|
||||||
ALERT_SCHEMA = vol.Schema({
|
ALERT_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||||
|
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
|
||||||
|
vol.Optional(CONF_DONE_MESSAGE): cv.template,
|
||||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
@@ -59,74 +62,49 @@ def is_on(hass, entity_id):
|
|||||||
return hass.states.is_state(entity_id, STATE_ON)
|
return hass.states.is_state(entity_id, STATE_ON)
|
||||||
|
|
||||||
|
|
||||||
def turn_on(hass, entity_id):
|
async def async_setup(hass, config):
|
||||||
"""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):
|
|
||||||
"""Set up the Alert component."""
|
"""Set up the Alert component."""
|
||||||
alerts = config.get(DOMAIN)
|
entities = []
|
||||||
all_alerts = {}
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
def async_handle_alert_service(service_call):
|
if not cfg:
|
||||||
|
cfg = {}
|
||||||
|
|
||||||
|
name = cfg.get(CONF_NAME)
|
||||||
|
watched_entity_id = cfg.get(CONF_ENTITY_ID)
|
||||||
|
alert_state = cfg.get(CONF_STATE)
|
||||||
|
repeat = cfg.get(CONF_REPEAT)
|
||||||
|
skip_first = cfg.get(CONF_SKIP_FIRST)
|
||||||
|
message_template = cfg.get(CONF_ALERT_MESSAGE)
|
||||||
|
done_message_template = cfg.get(CONF_DONE_MESSAGE)
|
||||||
|
notifiers = cfg.get(CONF_NOTIFIERS)
|
||||||
|
can_ack = cfg.get(CONF_CAN_ACK)
|
||||||
|
|
||||||
|
entities.append(Alert(hass, object_id, name,
|
||||||
|
watched_entity_id, alert_state, repeat,
|
||||||
|
skip_first, message_template,
|
||||||
|
done_message_template, notifiers,
|
||||||
|
can_ack))
|
||||||
|
|
||||||
|
if not entities:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_handle_alert_service(service_call):
|
||||||
"""Handle calls to alert services."""
|
"""Handle calls to alert services."""
|
||||||
alert_ids = service.extract_entity_ids(hass, service_call)
|
alert_ids = service.extract_entity_ids(hass, service_call)
|
||||||
|
|
||||||
for alert_id in alert_ids:
|
for alert_id in alert_ids:
|
||||||
alert = all_alerts[alert_id]
|
for alert in entities:
|
||||||
alert.async_set_context(service_call.context)
|
if alert.entity_id != alert_id:
|
||||||
if service_call.service == SERVICE_TURN_ON:
|
continue
|
||||||
yield from alert.async_turn_on()
|
|
||||||
elif service_call.service == SERVICE_TOGGLE:
|
|
||||||
yield from alert.async_toggle()
|
|
||||||
else:
|
|
||||||
yield from alert.async_turn_off()
|
|
||||||
|
|
||||||
# Setup alerts
|
alert.async_set_context(service_call.context)
|
||||||
for entity_id, alert in alerts.items():
|
if service_call.service == SERVICE_TURN_ON:
|
||||||
entity = Alert(hass, entity_id,
|
await alert.async_turn_on()
|
||||||
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
|
elif service_call.service == SERVICE_TOGGLE:
|
||||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
await alert.async_toggle()
|
||||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
else:
|
||||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
await alert.async_turn_off()
|
||||||
all_alerts[entity.entity_id] = entity
|
|
||||||
|
|
||||||
# Setup service calls
|
# Setup service calls
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
@@ -139,9 +117,9 @@ def async_setup(hass, config):
|
|||||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||||
schema=ALERT_SERVICE_SCHEMA)
|
schema=ALERT_SERVICE_SCHEMA)
|
||||||
|
|
||||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
tasks = [alert.async_update_ha_state() for alert in entities]
|
||||||
if tasks:
|
if tasks:
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -149,16 +127,25 @@ def async_setup(hass, config):
|
|||||||
class Alert(ToggleEntity):
|
class Alert(ToggleEntity):
|
||||||
"""Representation of an alert."""
|
"""Representation of an alert."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
def __init__(self, hass, entity_id, name, watched_entity_id,
|
||||||
state, repeat, skip_first, notifiers, can_ack):
|
state, repeat, skip_first, message_template,
|
||||||
|
done_message_template, notifiers, can_ack):
|
||||||
"""Initialize the alert."""
|
"""Initialize the alert."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._alert_state = state
|
self._alert_state = state
|
||||||
self._skip_first = skip_first
|
self._skip_first = skip_first
|
||||||
|
|
||||||
|
self._message_template = message_template
|
||||||
|
if self._message_template is not None:
|
||||||
|
self._message_template.hass = hass
|
||||||
|
|
||||||
|
self._done_message_template = done_message_template
|
||||||
|
if self._done_message_template is not None:
|
||||||
|
self._done_message_template.hass = hass
|
||||||
|
|
||||||
self._notifiers = notifiers
|
self._notifiers = notifiers
|
||||||
self._can_ack = can_ack
|
self._can_ack = can_ack
|
||||||
self._done_message = done_message
|
|
||||||
|
|
||||||
self._delay = [timedelta(minutes=val) for val in repeat]
|
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||||
self._next_delay = 0
|
self._next_delay = 0
|
||||||
@@ -196,17 +183,15 @@ class Alert(ToggleEntity):
|
|||||||
"""Hide the alert when it is not firing."""
|
"""Hide the alert when it is not firing."""
|
||||||
return not self._can_ack or not self._firing
|
return not self._can_ack or not self._firing
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def watched_entity_change(self, entity, from_state, to_state):
|
||||||
def watched_entity_change(self, entity, from_state, to_state):
|
|
||||||
"""Determine if the alert should start or stop."""
|
"""Determine if the alert should start or stop."""
|
||||||
_LOGGER.debug("Watched entity (%s) has changed", entity)
|
_LOGGER.debug("Watched entity (%s) has changed", entity)
|
||||||
if to_state.state == self._alert_state and not self._firing:
|
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:
|
if to_state.state != self._alert_state and self._firing:
|
||||||
yield from self.end_alerting()
|
await self.end_alerting()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def begin_alerting(self):
|
||||||
def begin_alerting(self):
|
|
||||||
"""Begin the alert procedures."""
|
"""Begin the alert procedures."""
|
||||||
_LOGGER.debug("Beginning Alert: %s", self._name)
|
_LOGGER.debug("Beginning Alert: %s", self._name)
|
||||||
self._ack = False
|
self._ack = False
|
||||||
@@ -214,25 +199,23 @@ class Alert(ToggleEntity):
|
|||||||
self._next_delay = 0
|
self._next_delay = 0
|
||||||
|
|
||||||
if not self._skip_first:
|
if not self._skip_first:
|
||||||
yield from self._notify()
|
await self._notify()
|
||||||
else:
|
else:
|
||||||
yield from self._schedule_notify()
|
await self._schedule_notify()
|
||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def end_alerting(self):
|
||||||
def end_alerting(self):
|
|
||||||
"""End the alert procedures."""
|
"""End the alert procedures."""
|
||||||
_LOGGER.debug("Ending Alert: %s", self._name)
|
_LOGGER.debug("Ending Alert: %s", self._name)
|
||||||
self._cancel()
|
self._cancel()
|
||||||
self._ack = False
|
self._ack = False
|
||||||
self._firing = False
|
self._firing = False
|
||||||
if self._done_message and self._send_done_message:
|
if self._send_done_message:
|
||||||
yield from self._notify_done_message()
|
await self._notify_done_message()
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _schedule_notify(self):
|
||||||
def _schedule_notify(self):
|
|
||||||
"""Schedule a notification."""
|
"""Schedule a notification."""
|
||||||
delay = self._delay[self._next_delay]
|
delay = self._delay[self._next_delay]
|
||||||
next_msg = datetime.now() + delay
|
next_msg = datetime.now() + delay
|
||||||
@@ -240,8 +223,7 @@ class Alert(ToggleEntity):
|
|||||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _notify(self, *args):
|
||||||
def _notify(self, *args):
|
|
||||||
"""Send the alert notification."""
|
"""Send the alert notification."""
|
||||||
if not self._firing:
|
if not self._firing:
|
||||||
return
|
return
|
||||||
@@ -249,37 +231,46 @@ class Alert(ToggleEntity):
|
|||||||
if not self._ack:
|
if not self._ack:
|
||||||
_LOGGER.info("Alerting: %s", self._name)
|
_LOGGER.info("Alerting: %s", self._name)
|
||||||
self._send_done_message = True
|
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()
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
if self._message_template is not None:
|
||||||
def _notify_done_message(self, *args):
|
message = self._message_template.async_render()
|
||||||
|
else:
|
||||||
|
message = self._name
|
||||||
|
|
||||||
|
await self._send_notification_message(message)
|
||||||
|
await self._schedule_notify()
|
||||||
|
|
||||||
|
async def _notify_done_message(self, *args):
|
||||||
"""Send notification of complete alert."""
|
"""Send notification of complete alert."""
|
||||||
_LOGGER.info("Alerting: %s", self._done_message)
|
_LOGGER.info("Alerting: %s", self._done_message_template)
|
||||||
self._send_done_message = False
|
self._send_done_message = False
|
||||||
for target in self._notifiers:
|
|
||||||
yield from self.hass.services.async_call(
|
|
||||||
'notify', target, {'message': self._done_message})
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
if self._done_message_template is None:
|
||||||
def async_turn_on(self, **kwargs):
|
return
|
||||||
|
|
||||||
|
message = self._done_message_template.async_render()
|
||||||
|
|
||||||
|
await self._send_notification_message(message)
|
||||||
|
|
||||||
|
async def _send_notification_message(self, message):
|
||||||
|
for target in self._notifiers:
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message})
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
"""Async Unacknowledge alert."""
|
"""Async Unacknowledge alert."""
|
||||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||||
self._ack = False
|
self._ack = False
|
||||||
yield from self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_off(self, **kwargs):
|
||||||
def async_turn_off(self, **kwargs):
|
|
||||||
"""Async Acknowledge alert."""
|
"""Async Acknowledge alert."""
|
||||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||||
self._ack = True
|
self._ack = True
|
||||||
yield from self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_toggle(self, **kwargs):
|
||||||
def async_toggle(self, **kwargs):
|
|
||||||
"""Async toggle alert."""
|
"""Async toggle alert."""
|
||||||
if self._ack:
|
if self._ack:
|
||||||
return self.async_turn_on()
|
return await self.async_turn_on()
|
||||||
return self.async_turn_off()
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alexa/
|
https://home-assistant.io/components/alexa/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -53,8 +52,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Activate Alexa component."""
|
"""Activate Alexa component."""
|
||||||
config = config.get(DOMAIN, {})
|
config = config.get(DOMAIN, {})
|
||||||
flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS)
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alexa/
|
https://home-assistant.io/components/alexa/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -59,16 +58,15 @@ class AlexaIntentsView(http.HomeAssistantView):
|
|||||||
url = INTENTS_API_ENDPOINT
|
url = INTENTS_API_ENDPOINT
|
||||||
name = 'api:alexa'
|
name = 'api:alexa'
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request):
|
||||||
def post(self, request):
|
|
||||||
"""Handle Alexa."""
|
"""Handle Alexa."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
message = yield from request.json()
|
message = await request.json()
|
||||||
|
|
||||||
_LOGGER.debug("Received Alexa request: %s", message)
|
_LOGGER.debug("Received Alexa request: %s", message)
|
||||||
|
|
||||||
try:
|
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)
|
return b'' if response is None else self.json(response)
|
||||||
except UnknownRequest as err:
|
except UnknownRequest as err:
|
||||||
_LOGGER.warning(str(err))
|
_LOGGER.warning(str(err))
|
||||||
@@ -101,8 +99,7 @@ def intent_error_response(hass, message, error):
|
|||||||
return alexa_response.as_dict()
|
return alexa_response.as_dict()
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_handle_message(hass, message):
|
||||||
def async_handle_message(hass, message):
|
|
||||||
"""Handle an Alexa intent.
|
"""Handle an Alexa intent.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -120,20 +117,18 @@ def async_handle_message(hass, message):
|
|||||||
if not handler:
|
if not handler:
|
||||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
||||||
|
|
||||||
return (yield from handler(hass, message))
|
return await handler(hass, message)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('SessionEndedRequest')
|
@HANDLERS.register('SessionEndedRequest')
|
||||||
@asyncio.coroutine
|
async def async_handle_session_end(hass, message):
|
||||||
def async_handle_session_end(hass, message):
|
|
||||||
"""Handle a session end request."""
|
"""Handle a session end request."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('IntentRequest')
|
@HANDLERS.register('IntentRequest')
|
||||||
@HANDLERS.register('LaunchRequest')
|
@HANDLERS.register('LaunchRequest')
|
||||||
@asyncio.coroutine
|
async def async_handle_intent(hass, message):
|
||||||
def async_handle_intent(hass, message):
|
|
||||||
"""Handle an intent request.
|
"""Handle an intent request.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -153,7 +148,7 @@ def async_handle_intent(hass, message):
|
|||||||
else:
|
else:
|
||||||
intent_name = alexa_intent_info['name']
|
intent_name = alexa_intent_info['name']
|
||||||
|
|
||||||
intent_response = yield from intent.async_handle(
|
intent_response = await intent.async_handle(
|
||||||
hass, DOMAIN, intent_name,
|
hass, DOMAIN, intent_name,
|
||||||
{key: {'value': value} for key, value
|
{key: {'value': value} for key, value
|
||||||
in alexa_response.variables.items()})
|
in alexa_response.variables.items()})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -149,16 +149,14 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Set up the IP Webcam component."""
|
"""Set up the IP Webcam component."""
|
||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
|
||||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_ipcamera(cam_config):
|
||||||
def async_setup_ipcamera(cam_config):
|
|
||||||
"""Set up an IP camera."""
|
"""Set up an IP camera."""
|
||||||
host = cam_config[CONF_HOST]
|
host = cam_config[CONF_HOST]
|
||||||
username = cam_config.get(CONF_USERNAME)
|
username = cam_config.get(CONF_USERNAME)
|
||||||
@@ -188,16 +186,15 @@ def async_setup(hass, config):
|
|||||||
if motion is None:
|
if motion is None:
|
||||||
motion = 'motion_active' in cam.enabled_sensors
|
motion = 'motion_active' in cam.enabled_sensors
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_update_data(now):
|
||||||
def async_update_data(now):
|
|
||||||
"""Update data from IP camera in SCAN_INTERVAL."""
|
"""Update data from IP camera in SCAN_INTERVAL."""
|
||||||
yield from cam.update()
|
await cam.update()
|
||||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||||
|
|
||||||
async_track_point_in_utc_time(
|
async_track_point_in_utc_time(
|
||||||
hass, async_update_data, utcnow() + interval)
|
hass, async_update_data, utcnow() + interval)
|
||||||
|
|
||||||
yield from async_update_data(None)
|
await async_update_data(None)
|
||||||
|
|
||||||
# Load platforms
|
# Load platforms
|
||||||
webcams[host] = cam
|
webcams[host] = cam
|
||||||
@@ -242,7 +239,7 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||||
if tasks:
|
if tasks:
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -255,8 +252,7 @@ class AndroidIPCamEntity(Entity):
|
|||||||
self._host = host
|
self._host = host
|
||||||
self._ipcam = ipcam
|
self._ipcam = ipcam
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register update dispatcher."""
|
"""Register update dispatcher."""
|
||||||
@callback
|
@callback
|
||||||
def async_ipcam_update(host):
|
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
|
# It doesn't really matter why we're not able to get the status, just that
|
||||||
# we can't.
|
# we can't.
|
||||||
# pylint: disable=broad-except
|
|
||||||
try:
|
try:
|
||||||
DATA.update(no_throttle=True)
|
DATA.update(no_throttle=True)
|
||||||
except Exception:
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ class APIEventStream(HomeAssistantView):
|
|||||||
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
|
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
|
||||||
unsub_stream()
|
unsub_stream()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class APIConfigView(HomeAssistantView):
|
class APIConfigView(HomeAssistantView):
|
||||||
"""View to handle Configuration requests."""
|
"""View to handle Configuration requests."""
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Sequence, TypeVar, Union
|
from typing import Sequence, TypeVar, Union
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -78,14 +77,13 @@ def request_configuration(hass, config, atv, credentials):
|
|||||||
"""Request configuration steps from the user."""
|
"""Request configuration steps from the user."""
|
||||||
configurator = hass.components.configurator
|
configurator = hass.components.configurator
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def configuration_callback(callback_data):
|
||||||
def configuration_callback(callback_data):
|
|
||||||
"""Handle the submitted configuration."""
|
"""Handle the submitted configuration."""
|
||||||
from pyatv import exceptions
|
from pyatv import exceptions
|
||||||
pin = callback_data.get('pin')
|
pin = callback_data.get('pin')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield from atv.airplay.finish_authentication(pin)
|
await atv.airplay.finish_authentication(pin)
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
'Authentication succeeded!<br /><br />Add the following '
|
'Authentication succeeded!<br /><br />Add the following '
|
||||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||||
@@ -109,11 +107,10 @@ def request_configuration(hass, config, atv, credentials):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def scan_for_apple_tvs(hass):
|
||||||
def scan_for_apple_tvs(hass):
|
|
||||||
"""Scan for devices and present a notification of the ones found."""
|
"""Scan for devices and present a notification of the ones found."""
|
||||||
import pyatv
|
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 = []
|
devices = []
|
||||||
for atv in atvs:
|
for atv in atvs:
|
||||||
@@ -133,14 +130,12 @@ def scan_for_apple_tvs(hass):
|
|||||||
notification_id=NOTIFICATION_SCAN_ID)
|
notification_id=NOTIFICATION_SCAN_ID)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Set up the Apple TV component."""
|
"""Set up the Apple TV component."""
|
||||||
if DATA_APPLE_TV not in hass.data:
|
if DATA_APPLE_TV not in hass.data:
|
||||||
hass.data[DATA_APPLE_TV] = {}
|
hass.data[DATA_APPLE_TV] = {}
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_service_handler(service):
|
||||||
def async_service_handler(service):
|
|
||||||
"""Handle service calls."""
|
"""Handle service calls."""
|
||||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
@@ -159,17 +154,16 @@ def async_setup(hass, config):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
atv = device.atv
|
atv = device.atv
|
||||||
credentials = yield from atv.airplay.generate_credentials()
|
credentials = await atv.airplay.generate_credentials()
|
||||||
yield from atv.airplay.load_credentials(credentials)
|
await atv.airplay.load_credentials(credentials)
|
||||||
_LOGGER.debug('Generated new credentials: %s', 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.async_add_job(request_configuration,
|
||||||
hass, config, atv, credentials)
|
hass, config, atv, credentials)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def atv_discovered(service, info):
|
||||||
def atv_discovered(service, info):
|
|
||||||
"""Set up an Apple TV that was auto discovered."""
|
"""Set up an Apple TV that was auto discovered."""
|
||||||
yield from _setup_atv(hass, {
|
await _setup_atv(hass, config, {
|
||||||
CONF_NAME: info['name'],
|
CONF_NAME: info['name'],
|
||||||
CONF_HOST: info['host'],
|
CONF_HOST: info['host'],
|
||||||
CONF_LOGIN_ID: info['properties']['hG'],
|
CONF_LOGIN_ID: info['properties']['hG'],
|
||||||
@@ -178,9 +172,9 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
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:
|
if tasks:
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||||
@@ -193,8 +187,7 @@ def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _setup_atv(hass, hass_config, atv_config):
|
||||||
def _setup_atv(hass, atv_config):
|
|
||||||
"""Set up an Apple TV."""
|
"""Set up an Apple TV."""
|
||||||
import pyatv
|
import pyatv
|
||||||
name = atv_config.get(CONF_NAME)
|
name = atv_config.get(CONF_NAME)
|
||||||
@@ -210,7 +203,7 @@ def _setup_atv(hass, atv_config):
|
|||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||||
if credentials:
|
if credentials:
|
||||||
yield from atv.airplay.load_credentials(credentials)
|
await atv.airplay.load_credentials(credentials)
|
||||||
|
|
||||||
power = AppleTVPowerManager(hass, atv, start_off)
|
power = AppleTVPowerManager(hass, atv, start_off)
|
||||||
hass.data[DATA_APPLE_TV][host] = {
|
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.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.async_create_task(discovery.async_load_platform(
|
||||||
hass, 'remote', DOMAIN, atv_config))
|
hass, 'remote', DOMAIN, atv_config, hass_config))
|
||||||
|
|
||||||
|
|
||||||
class AppleTVPowerManager:
|
class AppleTVPowerManager:
|
||||||
@@ -259,4 +252,4 @@ class AppleTVPowerManager:
|
|||||||
self.atv.push_updater.start()
|
self.atv.push_updater.start()
|
||||||
|
|
||||||
for listener in self.listeners:
|
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.event import track_time_interval
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
|
||||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
REQUIREMENTS = ['pyarlo==0.2.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def hub_refresh(event_time):
|
def hub_refresh(event_time):
|
||||||
"""Call ArloHub to refresh information."""
|
"""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,
|
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||||
update_base_station=True)
|
update_base_station=True)
|
||||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from homeassistant.core import callback
|
|||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect, async_dispatcher_send)
|
async_dispatcher_send, dispatcher_connect)
|
||||||
|
|
||||||
REQUIREMENTS = ['asterisk_mbox==0.5.0']
|
REQUIREMENTS = ['asterisk_mbox==0.5.0']
|
||||||
|
|
||||||
@@ -21,14 +21,17 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DOMAIN = 'asterisk_mbox'
|
DOMAIN = 'asterisk_mbox'
|
||||||
|
|
||||||
|
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
|
||||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
||||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
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({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_PORT): int,
|
vol.Required(CONF_PORT): cv.port,
|
||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@@ -41,9 +44,7 @@ def setup(hass, config):
|
|||||||
port = conf.get(CONF_PORT)
|
port = conf.get(CONF_PORT)
|
||||||
password = conf.get(CONF_PASSWORD)
|
password = conf.get(CONF_PASSWORD)
|
||||||
|
|
||||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
|
||||||
|
|
||||||
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -51,31 +52,71 @@ def setup(hass, config):
|
|||||||
class AsteriskData:
|
class AsteriskData:
|
||||||
"""Store Asterisk mailbox data."""
|
"""Store Asterisk mailbox data."""
|
||||||
|
|
||||||
def __init__(self, hass, host, port, password):
|
def __init__(self, hass, host, port, password, config):
|
||||||
"""Init the Asterisk data object."""
|
"""Init the Asterisk data object."""
|
||||||
from asterisk_mbox import Client as asteriskClient
|
from asterisk_mbox import Client as asteriskClient
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.client = asteriskClient(host, port, password, self.handle_data)
|
self.config = config
|
||||||
self.messages = []
|
self.messages = None
|
||||||
|
self.cdr = None
|
||||||
|
|
||||||
async_dispatcher_connect(
|
dispatcher_connect(
|
||||||
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
|
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
|
@callback
|
||||||
def handle_data(self, command, msg):
|
def handle_data(self, command, msg):
|
||||||
"""Handle changes to the mailbox."""
|
"""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:
|
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(
|
self.messages = sorted(
|
||||||
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
||||||
async_dispatcher_send(
|
if not isinstance(old_messages, list):
|
||||||
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
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
|
@callback
|
||||||
def _request_messages(self):
|
def _request_messages(self):
|
||||||
"""Handle changes to the mailbox."""
|
"""Handle changes to the mailbox."""
|
||||||
_LOGGER.debug("Requesting message list")
|
_LOGGER.debug("Requesting message list")
|
||||||
self.client.messages()
|
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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/august/
|
https://home-assistant.io/components/august/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@@ -124,6 +123,7 @@ def setup_august(hass, config, api, authenticator):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
if state == AuthenticationState.BAD_PASSWORD:
|
if state == AuthenticationState.BAD_PASSWORD:
|
||||||
|
_LOGGER.error("Invalid password provided")
|
||||||
return False
|
return False
|
||||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||||
request_configuration(hass, config, api, authenticator)
|
request_configuration(hass, config, api, authenticator)
|
||||||
@@ -165,6 +165,7 @@ class AugustData:
|
|||||||
self._doorbell_detail_by_id = {}
|
self._doorbell_detail_by_id = {}
|
||||||
self._lock_status_by_id = {}
|
self._lock_status_by_id = {}
|
||||||
self._lock_detail_by_id = {}
|
self._lock_detail_by_id = {}
|
||||||
|
self._door_state_by_id = {}
|
||||||
self._activities_by_id = {}
|
self._activities_by_id = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -184,6 +185,7 @@ class AugustData:
|
|||||||
|
|
||||||
def get_device_activities(self, device_id, *activity_types):
|
def get_device_activities(self, device_id, *activity_types):
|
||||||
"""Return a list of activities."""
|
"""Return a list of activities."""
|
||||||
|
_LOGGER.debug("Getting device activities")
|
||||||
self._update_device_activities()
|
self._update_device_activities()
|
||||||
|
|
||||||
activities = self._activities_by_id.get(device_id, [])
|
activities = self._activities_by_id.get(device_id, [])
|
||||||
@@ -199,6 +201,7 @@ class AugustData:
|
|||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||||
"""Update data object with latest from August API."""
|
"""Update data object with latest from August API."""
|
||||||
|
_LOGGER.debug("Updating device activities")
|
||||||
for house_id in self.house_ids:
|
for house_id in self.house_ids:
|
||||||
activities = self._api.get_house_activities(self._access_token,
|
activities = self._api.get_house_activities(self._access_token,
|
||||||
house_id,
|
house_id,
|
||||||
@@ -218,14 +221,30 @@ class AugustData:
|
|||||||
def _update_doorbells(self):
|
def _update_doorbells(self):
|
||||||
detail_by_id = {}
|
detail_by_id = {}
|
||||||
|
|
||||||
|
_LOGGER.debug("Start retrieving doorbell details")
|
||||||
for doorbell in self._doorbells:
|
for doorbell in self._doorbells:
|
||||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
_LOGGER.debug("Updating status for %s",
|
||||||
self._access_token, doorbell.device_id)
|
doorbell.device_name)
|
||||||
|
try:
|
||||||
|
detail_by_id[doorbell.device_id] =\
|
||||||
|
self._api.get_doorbell_detail(
|
||||||
|
self._access_token, doorbell.device_id)
|
||||||
|
except RequestException as ex:
|
||||||
|
_LOGGER.error("Request error trying to retrieve doorbell"
|
||||||
|
" status for %s. %s", doorbell.device_name, ex)
|
||||||
|
detail_by_id[doorbell.device_id] = None
|
||||||
|
except Exception:
|
||||||
|
detail_by_id[doorbell.device_id] = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
_LOGGER.debug("Completed retrieving doorbell details")
|
||||||
self._doorbell_detail_by_id = detail_by_id
|
self._doorbell_detail_by_id = detail_by_id
|
||||||
|
|
||||||
def get_lock_status(self, lock_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()
|
self._update_locks()
|
||||||
return self._lock_status_by_id.get(lock_id)
|
return self._lock_status_by_id.get(lock_id)
|
||||||
|
|
||||||
@@ -234,17 +253,69 @@ class AugustData:
|
|||||||
self._update_locks()
|
self._update_locks()
|
||||||
return self._lock_detail_by_id.get(lock_id)
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
||||||
|
self._access_token, lock.device_id)
|
||||||
|
except RequestException as ex:
|
||||||
|
_LOGGER.error("Request error trying to retrieve door"
|
||||||
|
" status for %s. %s", lock.device_name, ex)
|
||||||
|
state_by_id[lock.device_id] = None
|
||||||
|
except Exception:
|
||||||
|
state_by_id[lock.device_id] = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
_LOGGER.debug("Completed retrieving door status")
|
||||||
|
self._door_state_by_id = state_by_id
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def _update_locks(self):
|
def _update_locks(self):
|
||||||
status_by_id = {}
|
status_by_id = {}
|
||||||
detail_by_id = {}
|
detail_by_id = {}
|
||||||
|
|
||||||
|
_LOGGER.debug("Start retrieving locks status")
|
||||||
for lock in self._locks:
|
for lock in self._locks:
|
||||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
_LOGGER.debug("Updating status for %s",
|
||||||
self._access_token, lock.device_id)
|
lock.device_name)
|
||||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
try:
|
||||||
self._access_token, lock.device_id)
|
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||||
|
self._access_token, lock.device_id)
|
||||||
|
except RequestException as ex:
|
||||||
|
_LOGGER.error("Request error trying to retrieve door"
|
||||||
|
" status for %s. %s", lock.device_name, ex)
|
||||||
|
status_by_id[lock.device_id] = None
|
||||||
|
except Exception:
|
||||||
|
status_by_id[lock.device_id] = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||||
|
self._access_token, lock.device_id)
|
||||||
|
except RequestException as ex:
|
||||||
|
_LOGGER.error("Request error trying to retrieve door"
|
||||||
|
" details for %s. %s", lock.device_name, ex)
|
||||||
|
detail_by_id[lock.device_id] = None
|
||||||
|
except Exception:
|
||||||
|
detail_by_id[lock.device_id] = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
_LOGGER.debug("Completed retrieving locks status")
|
||||||
self._lock_status_by_id = status_by_id
|
self._lock_status_by_id = status_by_id
|
||||||
self._lock_detail_by_id = detail_by_id
|
self._lock_detail_by_id = detail_by_id
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
|
"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": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
@@ -7,7 +26,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"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.",
|
"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 \ub97c \uc0ac\uc6a9\ud558\uc5ec 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)"
|
"title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)"
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
22
homeassistant/components/auth/.translations/pt-BR.json
Normal file
22
homeassistant/components/auth/.translations/pt-BR.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"mfa_setup": {
|
||||||
|
"notify": {
|
||||||
|
"step": {
|
||||||
|
"setup": {
|
||||||
|
"title": "Verificar a configura\u00e7\u00e3o"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,31 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"mfa_setup": {
|
||||||
"totp": {
|
"notify": {
|
||||||
|
"abort": {
|
||||||
|
"no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel."
|
||||||
|
},
|
||||||
"error": {
|
"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."
|
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"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}`**.",
|
"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 Assistant \u00e9 preciso."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando palavras-passe de uso \u00fanico (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se 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 **`{code}`**.",
|
||||||
"title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP"
|
"title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
34
homeassistant/components/auth/.translations/ro.json
Normal file
34
homeassistant/components/auth/.translations/ro.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"mfa_setup": {
|
||||||
|
"notify": {
|
||||||
|
"abort": {
|
||||||
|
"no_available_service": "Nu sunt disponibile servicii de notificare."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_code": "Cod invalid, va rugam incercati din nou."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
|
||||||
|
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
|
||||||
|
"title": "Verifica\u021bi configurarea"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
|
||||||
|
},
|
||||||
|
"totp": {
|
||||||
|
"error": {
|
||||||
|
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "TOTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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."
|
"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."
|
||||||
|
|||||||
9
homeassistant/components/auth/.translations/uk.json
Normal file
9
homeassistant/components/auth/.translations/uk.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"mfa_setup": {
|
||||||
|
"notify": {
|
||||||
|
"error": {
|
||||||
|
"invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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"
|
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
{
|
{
|
||||||
"mfa_setup": {
|
"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": {
|
"totp": {
|
||||||
"error": {
|
"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"
|
"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": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
|
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
|
||||||
"title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49"
|
"title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ Home Assistant. User need to record the token in secure place.
|
|||||||
"id": 11,
|
"id": 11,
|
||||||
"type": "auth/long_lived_access_token",
|
"type": "auth/long_lived_access_token",
|
||||||
"client_name": "GPS Logger",
|
"client_name": "GPS Logger",
|
||||||
"client_icon": null,
|
|
||||||
"lifespan": 365
|
"lifespan": 365
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +129,7 @@ from homeassistant.auth.models import User, Credentials, \
|
|||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.http import KEY_REAL_IP
|
from homeassistant.components.http import KEY_REAL_IP
|
||||||
|
from homeassistant.components.http.auth import async_sign_path
|
||||||
from homeassistant.components.http.ban import log_invalid_auth
|
from homeassistant.components.http.ban import log_invalid_auth
|
||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
from homeassistant.components.http.view import HomeAssistantView
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
@@ -170,6 +170,14 @@ SCHEMA_WS_DELETE_REFRESH_TOKEN = \
|
|||||||
vol.Required('refresh_token_id'): str,
|
vol.Required('refresh_token_id'): str,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
WS_TYPE_SIGN_PATH = 'auth/sign_path'
|
||||||
|
SCHEMA_WS_SIGN_PATH = \
|
||||||
|
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_SIGN_PATH,
|
||||||
|
vol.Required('path'): str,
|
||||||
|
vol.Optional('expires', default=30): int,
|
||||||
|
})
|
||||||
|
|
||||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||||
RESULT_TYPE_USER = 'user'
|
RESULT_TYPE_USER = 'user'
|
||||||
|
|
||||||
@@ -202,6 +210,11 @@ async def async_setup(hass, config):
|
|||||||
websocket_delete_refresh_token,
|
websocket_delete_refresh_token,
|
||||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
||||||
)
|
)
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_SIGN_PATH,
|
||||||
|
websocket_sign_path,
|
||||||
|
SCHEMA_WS_SIGN_PATH
|
||||||
|
)
|
||||||
|
|
||||||
await login_flow.async_setup(hass, store_result)
|
await login_flow.async_setup(hass, store_result)
|
||||||
await mfa_setup_flow.async_setup(hass)
|
await mfa_setup_flow.async_setup(hass)
|
||||||
@@ -425,54 +438,46 @@ def _create_auth_code_store():
|
|||||||
|
|
||||||
|
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@websocket_api.async_response
|
||||||
def websocket_current_user(
|
async def websocket_current_user(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||||
"""Return the current user."""
|
"""Return the current user."""
|
||||||
async def async_get_current_user(user):
|
user = connection.user
|
||||||
"""Get current user."""
|
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(msg['id'], {
|
websocket_api.result_message(msg['id'], {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'is_owner': user.is_owner,
|
'is_owner': user.is_owner,
|
||||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||||
'auth_provider_id': c.auth_provider_id}
|
'auth_provider_id': c.auth_provider_id}
|
||||||
for c in user.credentials],
|
for c in user.credentials],
|
||||||
'mfa_modules': [{
|
'mfa_modules': [{
|
||||||
'id': module.id,
|
'id': module.id,
|
||||||
'name': module.name,
|
'name': module.name,
|
||||||
'enabled': module.id in enabled_modules,
|
'enabled': module.id in enabled_modules,
|
||||||
} for module in hass.auth.auth_mfa_modules],
|
} for module in hass.auth.auth_mfa_modules],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
hass.async_create_task(async_get_current_user(connection.user))
|
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@websocket_api.async_response
|
||||||
def websocket_create_long_lived_access_token(
|
async def websocket_create_long_lived_access_token(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||||
"""Create or a long-lived access token."""
|
"""Create or a long-lived access token."""
|
||||||
async def async_create_long_lived_access_token(user):
|
refresh_token = await hass.auth.async_create_refresh_token(
|
||||||
"""Create or a long-lived access token."""
|
connection.user,
|
||||||
refresh_token = await hass.auth.async_create_refresh_token(
|
client_name=msg['client_name'],
|
||||||
user,
|
client_icon=msg.get('client_icon'),
|
||||||
client_name=msg['client_name'],
|
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||||
client_icon=msg.get('client_icon'),
|
access_token_expiration=timedelta(days=msg['lifespan']))
|
||||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
|
||||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
|
||||||
|
|
||||||
access_token = hass.auth.async_create_access_token(
|
access_token = hass.auth.async_create_access_token(
|
||||||
refresh_token)
|
refresh_token)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(msg['id'], access_token))
|
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()
|
@websocket_api.ws_require_user()
|
||||||
@@ -480,8 +485,8 @@ def websocket_create_long_lived_access_token(
|
|||||||
def websocket_refresh_tokens(
|
def websocket_refresh_tokens(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||||
"""Return metadata of users refresh tokens."""
|
"""Return metadata of users refresh tokens."""
|
||||||
current_id = connection.request.get('refresh_token_id')
|
current_id = connection.refresh_token_id
|
||||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
|
connection.send_message(websocket_api.result_message(msg['id'], [{
|
||||||
'id': refresh.id,
|
'id': refresh.id,
|
||||||
'client_id': refresh.client_id,
|
'client_id': refresh.client_id,
|
||||||
'client_name': refresh.client_name,
|
'client_name': refresh.client_name,
|
||||||
@@ -495,22 +500,28 @@ def websocket_refresh_tokens(
|
|||||||
|
|
||||||
|
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@websocket_api.async_response
|
||||||
def websocket_delete_refresh_token(
|
async def websocket_delete_refresh_token(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||||
"""Handle a delete refresh token request."""
|
"""Handle a delete refresh token request."""
|
||||||
async def async_delete_refresh_token(user, refresh_token_id):
|
refresh_token = connection.user.refresh_tokens.get(msg['refresh_token_id'])
|
||||||
"""Delete a refresh token."""
|
|
||||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
|
||||||
|
|
||||||
if refresh_token is None:
|
if refresh_token is None:
|
||||||
return websocket_api.error_message(
|
return websocket_api.error_message(
|
||||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||||
|
|
||||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(msg['id'], {}))
|
websocket_api.result_message(msg['id'], {}))
|
||||||
|
|
||||||
hass.async_create_task(
|
|
||||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
@websocket_api.ws_require_user()
|
||||||
|
@callback
|
||||||
|
def websocket_sign_path(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||||
|
"""Handle a sign path request."""
|
||||||
|
connection.send_message(websocket_api.result_message(msg['id'], {
|
||||||
|
'path': async_sign_path(hass, connection.refresh_token_id, msg['path'],
|
||||||
|
timedelta(seconds=msg['expires']))
|
||||||
|
}))
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
"""Helpers to resolve client ID/secret."""
|
"""Helpers to resolve client ID/secret."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from ipaddress import ip_address
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from ipaddress import ip_address, ip_network
|
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.client_exceptions import ClientError
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
|
||||||
# IP addresses of loopback interfaces
|
from homeassistant.util.network import is_local
|
||||||
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'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||||
@@ -185,9 +174,7 @@ def _parse_client_id(client_id):
|
|||||||
# Not an ip address
|
# Not an ip address
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (address is None or
|
if address is None or is_local(address):
|
||||||
address in ALLOWED_IPS or
|
|
||||||
any(address in network for network in ALLOWED_NETWORKS)):
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
raise ValueError('Hostname should be a domain name or local IP address')
|
raise ValueError('Hostname should be a domain name or local IP address')
|
||||||
|
|||||||
@@ -226,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
|
|||||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||||
# need manually log failed login attempts
|
# need manually log failed login attempts
|
||||||
if result['errors'] is not None and \
|
if (result.get('errors') is not None and
|
||||||
result['errors'].get('base') == 'invalid_auth':
|
result['errors'].get('base') in ['invalid_auth',
|
||||||
|
'invalid_code']):
|
||||||
await process_wrong_login(request)
|
await process_wrong_login(request)
|
||||||
return self.json(_prepare_result_json(result))
|
return self.json(_prepare_result_json(result))
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def websocket_setup_mfa(
|
|||||||
if flow_id is not None:
|
if flow_id is not None:
|
||||||
result = await flow_manager.async_configure(
|
result = await flow_manager.async_configure(
|
||||||
flow_id, msg.get('user_input'))
|
flow_id, msg.get('user_input'))
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(
|
||||||
msg['id'], _prepare_result_json(result)))
|
msg['id'], _prepare_result_json(result)))
|
||||||
return
|
return
|
||||||
@@ -72,7 +72,7 @@ def websocket_setup_mfa(
|
|||||||
mfa_module_id = msg.get('mfa_module_id')
|
mfa_module_id = msg.get('mfa_module_id')
|
||||||
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
||||||
if mfa_module is None:
|
if mfa_module is None:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message(websocket_api.error_message(
|
||||||
msg['id'], 'no_module',
|
msg['id'], 'no_module',
|
||||||
'MFA module {} is not found'.format(mfa_module_id)))
|
'MFA module {} is not found'.format(mfa_module_id)))
|
||||||
return
|
return
|
||||||
@@ -80,7 +80,7 @@ def websocket_setup_mfa(
|
|||||||
result = await flow_manager.async_init(
|
result = await flow_manager.async_init(
|
||||||
mfa_module_id, data={'user_id': connection.user.id})
|
mfa_module_id, data={'user_id': connection.user.id})
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(
|
||||||
msg['id'], _prepare_result_json(result)))
|
msg['id'], _prepare_result_json(result)))
|
||||||
|
|
||||||
@@ -99,13 +99,13 @@ def websocket_depose_mfa(
|
|||||||
await hass.auth.async_disable_user_mfa(
|
await hass.auth.async_disable_user_mfa(
|
||||||
connection.user, msg['mfa_module_id'])
|
connection.user, msg['mfa_module_id'])
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message(websocket_api.error_message(
|
||||||
msg['id'], 'disable_failed',
|
msg['id'], 'disable_failed',
|
||||||
'Cannot disable MFA Module {}: {}'.format(
|
'Cannot disable MFA Module {}: {}'.format(
|
||||||
mfa_module_id, err)))
|
mfa_module_id, err)))
|
||||||
return
|
return
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(
|
||||||
msg['id'], 'done'))
|
msg['id'], 'done'))
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,25 @@
|
|||||||
"error": {
|
"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."
|
"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 (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||||
from homeassistant.components import logbook
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
@@ -115,49 +114,6 @@ def is_on(hass, entity_id):
|
|||||||
return hass.states.is_state(entity_id, STATE_ON)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the automation."""
|
"""Set up the automation."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||||
@@ -412,8 +368,8 @@ def _async_get_action(hass, config, name):
|
|||||||
async def action(entity_id, variables, context):
|
async def action(entity_id, variables, context):
|
||||||
"""Execute an action."""
|
"""Execute an action."""
|
||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info('Executing %s', name)
|
||||||
logbook.async_log_entry(
|
hass.components.logbook.async_log_entry(
|
||||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
name, 'has been triggered', DOMAIN, entity_id)
|
||||||
await script_obj.async_run(variables, context)
|
await script_obj.async_run(variables, context)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Offer event listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#event-trigger
|
at https://home-assistant.io/docs/automation/trigger/#event-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event_type = config.get(CONF_EVENT_TYPE)
|
event_type = config.get(CONF_EVENT_TYPE)
|
||||||
event_data_schema = vol.Schema(
|
event_data_schema = vol.Schema(
|
||||||
|
|||||||
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
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#homeassistant-trigger
|
at https://home-assistant.io/components/automation/#homeassistant-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -23,8 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/automation.litejet/
|
https://home-assistant.io/components/automation.litejet/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -33,8 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
number = config.get(CONF_NUMBER)
|
number = config.get(CONF_NUMBER)
|
||||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
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
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
|
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
topic = config.get(CONF_TOPIC)
|
topic = config.get(CONF_TOPIC)
|
||||||
payload = config.get(CONF_PAYLOAD)
|
payload = config.get(CONF_PAYLOAD)
|
||||||
@@ -51,6 +49,6 @@ def async_trigger(hass, config, action):
|
|||||||
'trigger': data
|
'trigger': data
|
||||||
})
|
})
|
||||||
|
|
||||||
remove = yield from mqtt.async_subscribe(
|
remove = await mqtt.async_subscribe(
|
||||||
hass, topic, mqtt_automation_listener)
|
hass, topic, mqtt_automation_listener)
|
||||||
return remove
|
return remove
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Offer numeric state listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
|
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
below = config.get(CONF_BELOW)
|
below = config.get(CONF_BELOW)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Offer state listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#state-trigger
|
at https://home-assistant.io/docs/automation/trigger/#state-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Offer sun based automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
|
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
offset = config.get(CONF_OFFSET)
|
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
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#template-trigger
|
at https://home-assistant.io/docs/automation/trigger/#template-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -23,8 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Offer time listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
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))
|
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
if CONF_AT in config:
|
if CONF_AT in config:
|
||||||
at_time = config.get(CONF_AT)
|
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
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
|
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_trigger(hass, config, action):
|
||||||
def async_trigger(hass, config, action):
|
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
zone_entity_id = config.get(CONF_ZONE)
|
zone_entity_id = config.get(CONF_ZONE)
|
||||||
|
|||||||
@@ -10,24 +10,21 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.discovery import SERVICE_AXIS
|
from homeassistant.components.discovery import SERVICE_AXIS
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
ATTR_LOCATION, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||||
EVENT_HOMEASSISTANT_STOP)
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
from homeassistant.helpers.entity import Entity
|
|
||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
|
|
||||||
REQUIREMENTS = ['axis==14']
|
REQUIREMENTS = ['axis==16']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'axis'
|
DOMAIN = 'axis'
|
||||||
CONFIG_FILE = 'axis.conf'
|
CONFIG_FILE = 'axis.conf'
|
||||||
|
|
||||||
AXIS_DEVICES = {}
|
|
||||||
|
|
||||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||||
'daynight', 'tampering', 'input']
|
'daynight', 'tampering', 'input']
|
||||||
|
|
||||||
@@ -38,6 +35,7 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
|||||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||||
AXIS_DEFAULT_USERNAME = 'root'
|
AXIS_DEFAULT_USERNAME = 'root'
|
||||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_INCLUDE):
|
vol.Required(CONF_INCLUDE):
|
||||||
@@ -47,7 +45,7 @@ DEVICE_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): 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_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,
|
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -98,8 +96,6 @@ def request_configuration(hass, config, name, host, serialnumber):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if setup_device(hass, config, device_config):
|
if setup_device(hass, config, device_config):
|
||||||
del device_config['events']
|
|
||||||
del device_config['signal']
|
|
||||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||||
config_file[serialnumber] = dict(device_config)
|
config_file[serialnumber] = dict(device_config)
|
||||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||||
@@ -145,9 +141,11 @@ def request_configuration(hass, config, name, host, serialnumber):
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up for Axis devices."""
|
"""Set up for Axis devices."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
|
||||||
def _shutdown(call):
|
def _shutdown(call):
|
||||||
"""Stop the event stream on shutdown."""
|
"""Stop the event stream on shutdown."""
|
||||||
for serialnumber, device in AXIS_DEVICES.items():
|
for serialnumber, device in hass.data[DOMAIN].items():
|
||||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||||
device.stop()
|
device.stop()
|
||||||
|
|
||||||
@@ -159,7 +157,7 @@ def setup(hass, config):
|
|||||||
name = discovery_info['hostname']
|
name = discovery_info['hostname']
|
||||||
serialnumber = discovery_info['properties']['macaddress']
|
serialnumber = discovery_info['properties']['macaddress']
|
||||||
|
|
||||||
if serialnumber not in AXIS_DEVICES:
|
if serialnumber not in hass.data[DOMAIN]:
|
||||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||||
if serialnumber in config_file:
|
if serialnumber in config_file:
|
||||||
# Device config previously saved to file
|
# Device config previously saved to file
|
||||||
@@ -177,7 +175,7 @@ def setup(hass, config):
|
|||||||
request_configuration(hass, config, name, host, serialnumber)
|
request_configuration(hass, config, name, host, serialnumber)
|
||||||
else:
|
else:
|
||||||
# Device already registered, but on a different IP
|
# Device already registered, but on a different IP
|
||||||
device = AXIS_DEVICES[serialnumber]
|
device = hass.data[DOMAIN][serialnumber]
|
||||||
device.config.host = host
|
device.config.host = host
|
||||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
||||||
|
|
||||||
@@ -194,7 +192,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def vapix_service(call):
|
def vapix_service(call):
|
||||||
"""Service to send a message."""
|
"""Service to send a message."""
|
||||||
for _, device in AXIS_DEVICES.items():
|
for device in hass.data[DOMAIN].values():
|
||||||
if device.name == call.data[CONF_NAME]:
|
if device.name == call.data[CONF_NAME]:
|
||||||
response = device.vapix.do_request(
|
response = device.vapix.do_request(
|
||||||
call.data[SERVICE_CGI],
|
call.data[SERVICE_CGI],
|
||||||
@@ -213,7 +211,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def setup_device(hass, config, device_config):
|
def setup_device(hass, config, device_config):
|
||||||
"""Set up an Axis device."""
|
"""Set up an Axis device."""
|
||||||
from axis import AxisDevice
|
import axis
|
||||||
|
|
||||||
def signal_callback(action, event):
|
def signal_callback(action, event):
|
||||||
"""Call to configure events when initialized on event stream."""
|
"""Call to configure events when initialized on event stream."""
|
||||||
@@ -228,18 +226,32 @@ def setup_device(hass, config, device_config):
|
|||||||
discovery.load_platform(
|
discovery.load_platform(
|
||||||
hass, component, DOMAIN, event_config, config)
|
hass, component, DOMAIN, event_config, config)
|
||||||
|
|
||||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
event_types = [
|
||||||
EVENT_TYPES))
|
event
|
||||||
device_config['events'] = event_types
|
for event in device_config[CONF_INCLUDE]
|
||||||
device_config['signal'] = signal_callback
|
if event in EVENT_TYPES
|
||||||
device = AxisDevice(hass.loop, **device_config)
|
]
|
||||||
device.name = device_config[CONF_NAME]
|
|
||||||
|
|
||||||
if device.serial_number is None:
|
device = axis.AxisDevice(
|
||||||
# If there is no serial number a connection could not be made
|
loop=hass.loop, host=device_config[CONF_HOST],
|
||||||
_LOGGER.error("Couldn't connect to %s", device_config[CONF_HOST])
|
username=device_config[CONF_USERNAME],
|
||||||
|
password=device_config[CONF_PASSWORD],
|
||||||
|
port=device_config[CONF_PORT], web_proto='http',
|
||||||
|
event_types=event_types, signal=signal_callback)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hass.data[DOMAIN][device.vapix.serial_number] = device
|
||||||
|
|
||||||
|
except axis.Unauthorized:
|
||||||
|
_LOGGER.error("Credentials for %s are faulty",
|
||||||
|
device_config[CONF_HOST])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
except axis.RequestError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
device.name = device_config[CONF_NAME]
|
||||||
|
|
||||||
for component in device_config[CONF_INCLUDE]:
|
for component in device_config[CONF_INCLUDE]:
|
||||||
if component == 'camera':
|
if component == 'camera':
|
||||||
camera_config = {
|
camera_config = {
|
||||||
@@ -252,51 +264,6 @@ def setup_device(hass, config, device_config):
|
|||||||
discovery.load_platform(
|
discovery.load_platform(
|
||||||
hass, component, DOMAIN, camera_config, config)
|
hass, component, DOMAIN, camera_config, config)
|
||||||
|
|
||||||
AXIS_DEVICES[device.serial_number] = device
|
|
||||||
if event_types:
|
if event_types:
|
||||||
hass.add_job(device.start)
|
hass.add_job(device.start)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AxisDeviceEvent(Entity):
|
|
||||||
"""Representation of a Axis device event."""
|
|
||||||
|
|
||||||
def __init__(self, event_config):
|
|
||||||
"""Initialize the event."""
|
|
||||||
self.axis_event = event_config[CONF_EVENT]
|
|
||||||
self._name = '{}_{}_{}'.format(
|
|
||||||
event_config[CONF_NAME], self.axis_event.event_type,
|
|
||||||
self.axis_event.id)
|
|
||||||
self.location = event_config[ATTR_LOCATION]
|
|
||||||
self.axis_event.callback = self._update_callback
|
|
||||||
|
|
||||||
def _update_callback(self):
|
|
||||||
"""Update the sensor's state, if needed."""
|
|
||||||
self.schedule_update_ha_state(True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the event."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_class(self):
|
|
||||||
"""Return the class of the event."""
|
|
||||||
return self.axis_event.event_class
|
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Return the polling state. No polling needed."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the state attributes of the event."""
|
|
||||||
attr = {}
|
|
||||||
|
|
||||||
tripped = self.axis_event.is_tripped
|
|
||||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
|
||||||
|
|
||||||
attr[ATTR_LOCATION] = self.location
|
|
||||||
|
|
||||||
return attr
|
|
||||||
15
homeassistant/components/axis/services.yaml
Normal file
15
homeassistant/components/axis/services.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
vapix_call:
|
||||||
|
description: Configure device using Vapix parameter management.
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
description: Name of device to Configure. [Required]
|
||||||
|
example: M1065-W
|
||||||
|
cgi:
|
||||||
|
description: Which cgi to call on device. [Optional] Default is 'param.cgi'
|
||||||
|
example: 'applications/control.cgi'
|
||||||
|
action:
|
||||||
|
description: What type of call. [Optional] Default is 'update'
|
||||||
|
example: 'start'
|
||||||
|
param:
|
||||||
|
description: What parameter to operate on. [Required]
|
||||||
|
example: 'package=VideoMotionDetection'
|
||||||
@@ -4,7 +4,6 @@ Support for ADS binary sensors.
|
|||||||
For more details about this platform, please refer to the documentation.
|
For more details about this platform, please refer to the documentation.
|
||||||
https://home-assistant.io/components/binary_sensor.ads/
|
https://home-assistant.io/components/binary_sensor.ads/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -50,8 +49,7 @@ class AdsBinarySensor(BinarySensorDevice):
|
|||||||
self._ads_hub = ads_hub
|
self._ads_hub = ads_hub
|
||||||
self.ads_var = ads_var
|
self.ads_var = ads_var
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register device notification."""
|
"""Register device notification."""
|
||||||
def update(name, value):
|
def update(name, value):
|
||||||
"""Handle device notifications."""
|
"""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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
@@ -64,8 +63,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
|||||||
self._relay_addr = relay_addr
|
self._relay_addr = relay_addr
|
||||||
self._relay_chan = relay_chan
|
self._relay_chan = relay_chan
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.android_ip_webcam import (
|
from homeassistant.components.android_ip_webcam import (
|
||||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
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']
|
DEPENDENCIES = ['android_ip_webcam']
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the IP Webcam binary sensors."""
|
"""Set up the IP Webcam binary sensors."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
@@ -51,8 +48,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
|||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_update(self):
|
||||||
def async_update(self):
|
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
state, _ = self._ipcam.export_sensor(self._sensor)
|
state, _ = self._ipcam.export_sensor(self._sensor)
|
||||||
self._state = state == 1.0
|
self._state = state == 1.0
|
||||||
|
|||||||
@@ -4,19 +4,30 @@ Support for August binary sensors.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.august/
|
https://home-assistant.io/components/sensor.august/
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from homeassistant.components.august import DATA_AUGUST
|
from homeassistant.components.august import DATA_AUGUST
|
||||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['august']
|
DEPENDENCIES = ['august']
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=5)
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
||||||
|
def _retrieve_door_state(data, lock):
|
||||||
|
"""Get the latest state of the DoorSense sensor."""
|
||||||
|
return data.get_door_state(lock.device_id)
|
||||||
|
|
||||||
|
|
||||||
def _retrieve_online_state(data, doorbell):
|
def _retrieve_online_state(data, doorbell):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||||
|
if detail is None:
|
||||||
|
return None
|
||||||
|
|
||||||
return detail.is_online
|
return detail.is_online
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +57,11 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
|||||||
|
|
||||||
|
|
||||||
# Sensor types: Name, device_class, state_provider
|
# 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_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
||||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
||||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
||||||
@@ -58,22 +73,51 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
data = hass.data[DATA_AUGUST]
|
data = hass.data[DATA_AUGUST]
|
||||||
devices = []
|
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 doorbell in data.doorbells:
|
||||||
for sensor_type in SENSOR_TYPES:
|
for sensor_type in SENSOR_TYPES_DOORBELL:
|
||||||
devices.append(AugustBinarySensor(data, sensor_type, 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)
|
add_entities(devices, True)
|
||||||
|
|
||||||
|
|
||||||
class AugustBinarySensor(BinarySensorDevice):
|
class AugustDoorBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of an August binary sensor."""
|
"""Representation of an August Door binary sensor."""
|
||||||
|
|
||||||
def __init__(self, data, sensor_type, doorbell):
|
def __init__(self, data, sensor_type, door):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._data = data
|
self._data = data
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._doorbell = doorbell
|
self._door = door
|
||||||
self._state = None
|
self._state = None
|
||||||
|
self._available = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return the availability of this sensor."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@@ -83,15 +127,58 @@ class AugustBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
return SENSOR_TYPES[self._sensor_type][1]
|
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)
|
||||||
|
self._available = self._state is not None
|
||||||
|
|
||||||
|
from august.lock import LockDoorStatus
|
||||||
|
self._state = self._state == LockDoorStatus.OPEN
|
||||||
|
|
||||||
|
|
||||||
|
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||||
|
"""Representation of an August binary sensor."""
|
||||||
|
|
||||||
|
def __init__(self, data, sensor_type, doorbell):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._data = data
|
||||||
|
self._sensor_type = sensor_type
|
||||||
|
self._doorbell = doorbell
|
||||||
|
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_DOORBELL[self._sensor_type][1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the binary sensor."""
|
"""Return the name of the binary sensor."""
|
||||||
return "{} {}".format(self._doorbell.device_name,
|
return "{} {}".format(self._doorbell.device_name,
|
||||||
SENSOR_TYPES[self._sensor_type][0])
|
SENSOR_TYPES_DOORBELL[self._sensor_type][0])
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""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)
|
self._state = state_provider(self._data, self._doorbell)
|
||||||
|
self._available = self._state is not None
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.axis/
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.axis import AxisDeviceEvent
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.const import CONF_TRIGGER_TIME
|
from homeassistant.const import (
|
||||||
from homeassistant.helpers.event import track_point_in_utc_time
|
ATTR_LOCATION, CONF_EVENT, CONF_NAME, CONF_TRIGGER_TIME)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
DEPENDENCIES = ['axis']
|
DEPENDENCIES = ['axis']
|
||||||
@@ -20,48 +21,71 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Axis binary devices."""
|
"""Set up the Axis binary devices."""
|
||||||
add_entities([AxisBinarySensor(hass, discovery_info)], True)
|
add_entities([AxisBinarySensor(discovery_info)], True)
|
||||||
|
|
||||||
|
|
||||||
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
class AxisBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of a binary Axis event."""
|
"""Representation of a binary Axis event."""
|
||||||
|
|
||||||
def __init__(self, hass, event_config):
|
def __init__(self, event_config):
|
||||||
"""Initialize the Axis binary sensor."""
|
"""Initialize the Axis binary sensor."""
|
||||||
self.hass = hass
|
self.axis_event = event_config[CONF_EVENT]
|
||||||
self._state = False
|
self.device_name = event_config[CONF_NAME]
|
||||||
self._delay = event_config[CONF_TRIGGER_TIME]
|
self.location = event_config[ATTR_LOCATION]
|
||||||
self._timer = None
|
self.delay = event_config[CONF_TRIGGER_TIME]
|
||||||
AxisDeviceEvent.__init__(self, event_config)
|
self.remove_timer = None
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Subscribe sensors events."""
|
||||||
|
self.axis_event.callback = self._update_callback
|
||||||
|
|
||||||
|
def _update_callback(self):
|
||||||
|
"""Update the sensor's state, if needed."""
|
||||||
|
if self.remove_timer is not None:
|
||||||
|
self.remove_timer()
|
||||||
|
self.remove_timer = None
|
||||||
|
|
||||||
|
if self.delay == 0 or self.is_on:
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
else: # Run timer to delay updating the state
|
||||||
|
@callback
|
||||||
|
def _delay_update(now):
|
||||||
|
"""Timer callback for sensor update."""
|
||||||
|
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||||
|
self.name, self.delay)
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
self.remove_timer = None
|
||||||
|
|
||||||
|
self.remove_timer = async_track_point_in_utc_time(
|
||||||
|
self.hass, _delay_update,
|
||||||
|
utcnow() + timedelta(seconds=self.delay))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if event is active."""
|
"""Return true if event is active."""
|
||||||
return self._state
|
return self.axis_event.is_tripped
|
||||||
|
|
||||||
def update(self):
|
@property
|
||||||
"""Get the latest data and update the state."""
|
def name(self):
|
||||||
self._state = self.axis_event.is_tripped
|
"""Return the name of the event."""
|
||||||
|
return '{}_{}_{}'.format(
|
||||||
|
self.device_name, self.axis_event.event_type, self.axis_event.id)
|
||||||
|
|
||||||
def _update_callback(self):
|
@property
|
||||||
"""Update the sensor's state, if needed."""
|
def device_class(self):
|
||||||
self.update()
|
"""Return the class of the event."""
|
||||||
|
return self.axis_event.event_class
|
||||||
|
|
||||||
if self._timer is not None:
|
@property
|
||||||
self._timer()
|
def should_poll(self):
|
||||||
self._timer = None
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
if self._delay > 0 and not self.is_on:
|
@property
|
||||||
# Set timer to wait until updating the state
|
def device_state_attributes(self):
|
||||||
def _delay_update(now):
|
"""Return the state attributes of the event."""
|
||||||
"""Timer callback for sensor update."""
|
attr = {}
|
||||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
|
||||||
self._name, self._delay)
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
self._timer = None
|
|
||||||
|
|
||||||
self._timer = track_point_in_utc_time(
|
attr[ATTR_LOCATION] = self.location
|
||||||
self.hass, _delay_update,
|
|
||||||
utcnow() + timedelta(seconds=self._delay))
|
return attr
|
||||||
else:
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user