mirror of
https://github.com/home-assistant/core.git
synced 2026-01-09 09:07:16 +01:00
Compare commits
881 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f8c91d77c | ||
|
|
ef5095cf53 | ||
|
|
5015071816 | ||
|
|
b110a80fbd | ||
|
|
c482d48fde | ||
|
|
7d9f8b0d4c | ||
|
|
b8981b2675 | ||
|
|
6028db21ab | ||
|
|
c63fd974fb | ||
|
|
cdb86ed154 | ||
|
|
0f844311c9 | ||
|
|
8d2359026c | ||
|
|
a5112f317d | ||
|
|
3ed47b05a5 | ||
|
|
163cd72b7a | ||
|
|
5e71f0f0d7 | ||
|
|
be61e2e714 | ||
|
|
1e5596b594 | ||
|
|
744c277123 | ||
|
|
460bb69ade | ||
|
|
8dbe78a21a | ||
|
|
3959f82030 | ||
|
|
48ba13bc6c | ||
|
|
681082a3ad | ||
|
|
4013a90f33 | ||
|
|
316ef89541 | ||
|
|
a8dd81e986 | ||
|
|
4b257c3d01 | ||
|
|
491bc006b2 | ||
|
|
28ad0017e1 | ||
|
|
5849381dfb | ||
|
|
baa974a487 | ||
|
|
1a97ba1b46 | ||
|
|
1d68f4e279 | ||
|
|
a2b793c61b | ||
|
|
93d6fb8c60 | ||
|
|
c7f4bdafc0 | ||
|
|
867f80715e | ||
|
|
29e668e887 | ||
|
|
944f4f7c05 | ||
|
|
cd6544d32a | ||
|
|
b2f4bbf93b | ||
|
|
a99b4472a8 | ||
|
|
33f3e72dda | ||
|
|
e30510a688 | ||
|
|
974fe4d923 | ||
|
|
feb8aff46b | ||
|
|
eee9b50b70 | ||
|
|
9fb8bc8991 | ||
|
|
1c42caba76 | ||
|
|
9d59bfbe00 | ||
|
|
95dc06cca6 | ||
|
|
9ecbf86fa0 | ||
|
|
588fd1923f | ||
|
|
2824efd505 | ||
|
|
169c8d793a | ||
|
|
68f03dcc67 | ||
|
|
397f551e6d | ||
|
|
cbb5d34167 | ||
|
|
0cc9798c8f | ||
|
|
45a7ca62ae | ||
|
|
2eb125e90e | ||
|
|
264c618b11 | ||
|
|
d9cf8fcfe8 | ||
|
|
5e9c1098c0 | ||
|
|
d65bd7b7ea | ||
|
|
45a5ae1f23 | ||
|
|
3eda6db227 | ||
|
|
58f287f551 | ||
|
|
f62f64311d | ||
|
|
fbeaa57604 | ||
|
|
c1f5ead61d | ||
|
|
d7690c5fda | ||
|
|
45c35ceb2b | ||
|
|
bc481fa366 | ||
|
|
1b94fe3613 | ||
|
|
3204501174 | ||
|
|
f3dfc433c2 | ||
|
|
8213b1476f | ||
|
|
4e7dbf9ce5 | ||
|
|
ea2ff6aae3 | ||
|
|
50b6c5948d | ||
|
|
3acbd5a769 | ||
|
|
fddfb9e412 | ||
|
|
1325682d82 | ||
|
|
140a874917 | ||
|
|
b7c336a687 | ||
|
|
a38c0d6d15 | ||
|
|
75f40ccb06 | ||
|
|
4de847f84e | ||
|
|
33f1577dac | ||
|
|
ef3a83048c | ||
|
|
ae2ee8f006 | ||
|
|
6f6d86c700 | ||
|
|
d1b16e287c | ||
|
|
ee8a815e6b | ||
|
|
7bc2362e33 | ||
|
|
9a8389060c | ||
|
|
da3366859d | ||
|
|
200c0a8778 | ||
|
|
5cf9cd686c | ||
|
|
8e659baf25 | ||
|
|
2aa54ce22b | ||
|
|
eff334a1d0 | ||
|
|
b3bed7fb37 | ||
|
|
61b3822374 | ||
|
|
9fb04b5280 | ||
|
|
3341c5cf21 | ||
|
|
f1286f8e6b | ||
|
|
f2a99e83cd | ||
|
|
2f7b79764a | ||
|
|
ea18e06b08 | ||
|
|
a0193e8e42 | ||
|
|
2fcacbff23 | ||
|
|
a42288d056 | ||
|
|
7aa2a9e506 | ||
|
|
2fc0d83085 | ||
|
|
ca0d4226aa | ||
|
|
dff2e4ebc2 | ||
|
|
9c337bc621 | ||
|
|
5a1360678b | ||
|
|
33ee91a748 | ||
|
|
396895d077 | ||
|
|
8b04d48ffd | ||
|
|
2a76a0852f | ||
|
|
22d961de70 | ||
|
|
4650366f07 | ||
|
|
7b8ad64ba5 | ||
|
|
e64761b15e | ||
|
|
61273ff606 | ||
|
|
dfe17491f8 | ||
|
|
a8c7425e17 | ||
|
|
e5f0da75e2 | ||
|
|
6834e00be6 | ||
|
|
26375a3014 | ||
|
|
06c3f756b1 | ||
|
|
9c5bbfe96d | ||
|
|
e427f9ee38 | ||
|
|
e62e2bb131 | ||
|
|
bf17ed0917 | ||
|
|
058081b1f5 | ||
|
|
98722e10fc | ||
|
|
2781796d9c | ||
|
|
24d2261060 | ||
|
|
7d7c2104ea | ||
|
|
4ab502a691 | ||
|
|
9292d9255c | ||
|
|
2022d39339 | ||
|
|
e31dd4404e | ||
|
|
5dc29bd2c3 | ||
|
|
20c316bce4 | ||
|
|
8b475f45e9 | ||
|
|
a4318682f7 | ||
|
|
a14d8057ed | ||
|
|
d2f4bce6c0 | ||
|
|
b0a3207454 | ||
|
|
db3cdb288e | ||
|
|
8797cb78a9 | ||
|
|
7eb5cd1267 | ||
|
|
0b2aff61bb | ||
|
|
55f8b0a2f5 | ||
|
|
bb37300a48 | ||
|
|
0f12b37977 | ||
|
|
ad4cba70a0 | ||
|
|
dd7890c848 | ||
|
|
7f18739267 | ||
|
|
a1b478b3ac | ||
|
|
edf1f44668 | ||
|
|
60f780cc37 | ||
|
|
7d0cc7e26c | ||
|
|
864a254071 | ||
|
|
5995c6a2ac | ||
|
|
ed0cfc4f31 | ||
|
|
6db069881b | ||
|
|
ca4f69f557 | ||
|
|
37ccf87516 | ||
|
|
201c9fed77 | ||
|
|
3b5775573b | ||
|
|
6e22a0e4d9 | ||
|
|
ce5b4cd51e | ||
|
|
538236de8f | ||
|
|
1007bb83aa | ||
|
|
79955a5785 | ||
|
|
e60f9ca392 | ||
|
|
ae581694ac | ||
|
|
70fe463ef0 | ||
|
|
84858f5c19 | ||
|
|
a6ba5ec1c8 | ||
|
|
c2fe0d0120 | ||
|
|
b6ca03ce47 | ||
|
|
23f1b49e55 | ||
|
|
6e3ec97acf | ||
|
|
4a6afc5614 | ||
|
|
b557c17f76 | ||
|
|
c587536547 | ||
|
|
4c6394b307 | ||
|
|
534233388c | ||
|
|
43b31e88ba | ||
|
|
6197fe0121 | ||
|
|
1f6331c69d | ||
|
|
fd568d77c7 | ||
|
|
f32098abe4 | ||
|
|
b65d7daed8 | ||
|
|
9ea0c409e6 | ||
|
|
2ee62b10bc | ||
|
|
dbdd0a1f56 | ||
|
|
df8c59406b | ||
|
|
c5a2ffbcb9 | ||
|
|
e62bb299ff | ||
|
|
6ee8d9bd65 | ||
|
|
14a34f8c4b | ||
|
|
3b93fa80be | ||
|
|
57977bcef3 | ||
|
|
0d4841cbea | ||
|
|
f7d7d825b0 | ||
|
|
1d1408b98d | ||
|
|
b9eb0081cd | ||
|
|
287b1bce15 | ||
|
|
ec3d2e97e8 | ||
|
|
1ff329d9d6 | ||
|
|
703d71c064 | ||
|
|
a2a4c633f3 | ||
|
|
e6dd4f6e13 | ||
|
|
b327ea2023 | ||
|
|
b333dba875 | ||
|
|
02238b6412 | ||
|
|
bd62248841 | ||
|
|
dabbd7bd63 | ||
|
|
b5c7afcf75 | ||
|
|
f8f8da959a | ||
|
|
9970965718 | ||
|
|
a1d8b0e9b3 | ||
|
|
1e7cfc04af | ||
|
|
0f1bcfd63b | ||
|
|
f65c3940ae | ||
|
|
46de89e1a3 | ||
|
|
852526e10a | ||
|
|
91d6d0df84 | ||
|
|
cb129bd207 | ||
|
|
a6e9dc81aa | ||
|
|
5f7ac09a74 | ||
|
|
42775142f8 | ||
|
|
2525fc52b3 | ||
|
|
07dde62e70 | ||
|
|
cb458b7745 | ||
|
|
b2df199674 | ||
|
|
857c58c4b7 | ||
|
|
b82371f44b | ||
|
|
1c525968d1 | ||
|
|
5ec61e4649 | ||
|
|
184d0a99c0 | ||
|
|
232f56de62 | ||
|
|
66e33c7979 | ||
|
|
6420ab5535 | ||
|
|
ed3fe1cc6f | ||
|
|
cd1cfd7e8e | ||
|
|
31e23ebae2 | ||
|
|
fb65276daf | ||
|
|
bedd2d7e41 | ||
|
|
120111ceee | ||
|
|
e6390b8e41 | ||
|
|
d7fd9247a9 | ||
|
|
0dc155c4d3 | ||
|
|
0feb4c5439 | ||
|
|
f3588a8782 | ||
|
|
2145ac5e46 | ||
|
|
c39e6b9618 | ||
|
|
855cbc0aed | ||
|
|
00c366d7ea | ||
|
|
3c3a53a137 | ||
|
|
dd59054003 | ||
|
|
36f566a529 | ||
|
|
4d93a9fd38 | ||
|
|
d3df96a8de | ||
|
|
6c77702dcc | ||
|
|
86165750ff | ||
|
|
63b28aa39d | ||
|
|
279fd39677 | ||
|
|
c978281d1e | ||
|
|
311a44007c | ||
|
|
47401739ea | ||
|
|
11ba7cc8ce | ||
|
|
c3ad30ec87 | ||
|
|
a64a66dd62 | ||
|
|
dffe36761d | ||
|
|
0a186650bf | ||
|
|
6c77c9d372 | ||
|
|
4a4b9180d8 | ||
|
|
5d6db9a915 | ||
|
|
235282e335 | ||
|
|
6f582dcf24 | ||
|
|
9db8759317 | ||
|
|
136cc1d44d | ||
|
|
4c258ce08b | ||
|
|
3c04b0756f | ||
|
|
c0229ebb77 | ||
|
|
cfe7c0aa01 | ||
|
|
f874efb224 | ||
|
|
3da4642194 | ||
|
|
0aad056ca7 | ||
|
|
c5ceb40598 | ||
|
|
27a37e2013 | ||
|
|
10d1e81f10 | ||
|
|
56bbadb501 | ||
|
|
fa79aead9a | ||
|
|
24fec3e826 | ||
|
|
2524dca7bf | ||
|
|
56f17b8651 | ||
|
|
49623d2dad | ||
|
|
66479dc2e5 | ||
|
|
bbbec5a056 | ||
|
|
94b55efef3 | ||
|
|
fd38caa287 | ||
|
|
c61a652c90 | ||
|
|
e3e014bccc | ||
|
|
26590e244c | ||
|
|
39971ee919 | ||
|
|
2205090795 | ||
|
|
a277470363 | ||
|
|
19f2bbf52f | ||
|
|
dbb786c548 | ||
|
|
4fbe3bb070 | ||
|
|
9066ac44fe | ||
|
|
742144f401 | ||
|
|
c0b6a857f7 | ||
|
|
d6dee62c92 | ||
|
|
41017f10a3 | ||
|
|
ba50a5c329 | ||
|
|
4208bb457d | ||
|
|
15af6b1ad9 | ||
|
|
3921dc77a6 | ||
|
|
0094fd5c34 | ||
|
|
d58e401812 | ||
|
|
c79c94550f | ||
|
|
9b950f5192 | ||
|
|
2520fddbdf | ||
|
|
3f21966ec9 | ||
|
|
69502163bd | ||
|
|
893e0f8db6 | ||
|
|
1c8b52f630 | ||
|
|
6e4fb7a937 | ||
|
|
ab1939f56f | ||
|
|
15507df407 | ||
|
|
46ea28a4f8 | ||
|
|
c8458fd7c5 | ||
|
|
e681a7929c | ||
|
|
b2d37ccef6 | ||
|
|
9dd2c36de4 | ||
|
|
b92350fb55 | ||
|
|
6c0fc65eaf | ||
|
|
42ba2a68ce | ||
|
|
508d0459a7 | ||
|
|
dbae410cf4 | ||
|
|
ae51dc08bf | ||
|
|
672a3c7178 | ||
|
|
f8bc3411ad | ||
|
|
038168c417 | ||
|
|
73034c933e | ||
|
|
d3ceb9080c | ||
|
|
3893d8a876 | ||
|
|
05924a2868 | ||
|
|
021d08a9c4 | ||
|
|
5a71a22fb9 | ||
|
|
6064932e2e | ||
|
|
9de7034d0e | ||
|
|
96d5684a89 | ||
|
|
91962e2681 | ||
|
|
ee31f89049 | ||
|
|
370c3f28b8 | ||
|
|
66110a7d57 | ||
|
|
c419cbb46f | ||
|
|
a02d7989d5 | ||
|
|
7325847fa9 | ||
|
|
124495dd84 | ||
|
|
0c01f3a0fe | ||
|
|
0ea2d99910 | ||
|
|
6456f66b47 | ||
|
|
94eee6d069 | ||
|
|
6e5a2a77ab | ||
|
|
35b609dd8b | ||
|
|
0df99f8762 | ||
|
|
6781ecf159 | ||
|
|
a4b843eb2d | ||
|
|
302717e8a1 | ||
|
|
617647c5fd | ||
|
|
4b5d578c08 | ||
|
|
bfc55137ea | ||
|
|
e98e7e2751 | ||
|
|
b687de879c | ||
|
|
4048ad36a8 | ||
|
|
8c2f0e3b30 | ||
|
|
6cabbd2592 | ||
|
|
8d22754a06 | ||
|
|
be6d1b5e94 | ||
|
|
c84f1d7d33 | ||
|
|
49845d9398 | ||
|
|
895306f822 | ||
|
|
a729742757 | ||
|
|
6bc03ee763 | ||
|
|
75580dfade | ||
|
|
1f8699d9b4 | ||
|
|
fca5d55b43 | ||
|
|
659616a4eb | ||
|
|
3b4f7b4f5d | ||
|
|
62432ced90 | ||
|
|
27873b4457 | ||
|
|
1e7333eeb6 | ||
|
|
7cd620d30f | ||
|
|
7a180ac205 | ||
|
|
153ccda853 | ||
|
|
067e4f6d9a | ||
|
|
9d6ce609f9 | ||
|
|
9800b74a6d | ||
|
|
ef5b2a2492 | ||
|
|
60179a1cbb | ||
|
|
e0cea2d18d | ||
|
|
e29dfa8609 | ||
|
|
ef39bca52e | ||
|
|
5a3ea74a26 | ||
|
|
8869617890 | ||
|
|
62f970e486 | ||
|
|
f9a21dbfda | ||
|
|
86c6b4d8e3 | ||
|
|
7bfa81c592 | ||
|
|
d07e40c483 | ||
|
|
0e7e58f172 | ||
|
|
cbdfc95cc8 | ||
|
|
1642502a70 | ||
|
|
33ebd99068 | ||
|
|
9c17e95fc5 | ||
|
|
1533bc1e1f | ||
|
|
da3695dccc | ||
|
|
40c8f5f70e | ||
|
|
3ceee66e1b | ||
|
|
6b908b6f4e | ||
|
|
a74b081d44 | ||
|
|
bc8093c73b | ||
|
|
ca2712506b | ||
|
|
c871e8da5d | ||
|
|
722c27f1e2 | ||
|
|
e3fcf46566 | ||
|
|
1117371b31 | ||
|
|
addca54118 | ||
|
|
471d6e45eb | ||
|
|
7238205adb | ||
|
|
3db5d5bbf9 | ||
|
|
65970a2248 | ||
|
|
5d82f48c02 | ||
|
|
8e185bc300 | ||
|
|
a013908115 | ||
|
|
bdf6257640 | ||
|
|
1f50e335fa | ||
|
|
1375adfeab | ||
|
|
00cbdffa12 | ||
|
|
c5f012c85a | ||
|
|
656eae288e | ||
|
|
5898307715 | ||
|
|
9b0efdc8c8 | ||
|
|
87f9f17335 | ||
|
|
f101f6b7cb | ||
|
|
abf07b60f0 | ||
|
|
0b114f0755 | ||
|
|
3ee8f58fdf | ||
|
|
ff4da05267 | ||
|
|
17308a2730 | ||
|
|
7d9bce2153 | ||
|
|
2839f0ff5f | ||
|
|
2ec295a6f8 | ||
|
|
4bd7a7eee3 | ||
|
|
d0cbbe6141 | ||
|
|
9efa31ef9f | ||
|
|
8a777f6e78 | ||
|
|
ac13a2736b | ||
|
|
940577e105 | ||
|
|
c917470836 | ||
|
|
47a344f3a1 | ||
|
|
f744a29d9d | ||
|
|
3cd4cb741c | ||
|
|
1128104281 | ||
|
|
d6d685a483 | ||
|
|
2c6e6c2a6f | ||
|
|
c8e0de19b6 | ||
|
|
b2440a6d95 | ||
|
|
c36c3f0d64 | ||
|
|
0e7d284c83 | ||
|
|
cdd111df49 | ||
|
|
cccd0deb65 | ||
|
|
e014a84215 | ||
|
|
d549e26a9b | ||
|
|
08adfd87f7 | ||
|
|
65b0ec6615 | ||
|
|
cb646e48d0 | ||
|
|
fecce206a9 | ||
|
|
176ef411de | ||
|
|
2ac23c8be6 | ||
|
|
a373793029 | ||
|
|
3153b0c8fc | ||
|
|
89d008d1f3 | ||
|
|
6755ae2605 | ||
|
|
c18033ba85 | ||
|
|
cdc5388dc9 | ||
|
|
be4776d039 | ||
|
|
30111ea417 | ||
|
|
576c806e86 | ||
|
|
1c561eaf0d | ||
|
|
1da30032a0 | ||
|
|
d5bbb6ffd2 | ||
|
|
b4e5695bbd | ||
|
|
716ab0433f | ||
|
|
7d9ef97bda | ||
|
|
703b4354e0 | ||
|
|
ce0ca7ff90 | ||
|
|
54e87836f6 | ||
|
|
5f4aa6d2ba | ||
|
|
dc447a75c6 | ||
|
|
ce7e9e36dd | ||
|
|
8aca2e84dc | ||
|
|
f3e55ce330 | ||
|
|
20caeb5383 | ||
|
|
bc0d0751b9 | ||
|
|
5393b073fe | ||
|
|
d7b7370c82 | ||
|
|
5f65f67f1e | ||
|
|
f242418986 | ||
|
|
d3d9d9ebf2 | ||
|
|
8ceb57752b | ||
|
|
bd1af8c3d8 | ||
|
|
6af995026b | ||
|
|
19a30b0ce6 | ||
|
|
9a659a5d1d | ||
|
|
1cfd770b95 | ||
|
|
3fda97eed7 | ||
|
|
b657cff6ba | ||
|
|
e3fba79126 | ||
|
|
bb0068908d | ||
|
|
0748466ffc | ||
|
|
fe018fd58c | ||
|
|
10317a0f71 | ||
|
|
87d55834be | ||
|
|
d4cc806cd5 | ||
|
|
1a7e8c88a3 | ||
|
|
90a51160c4 | ||
|
|
50321a29b5 | ||
|
|
67d137cfd5 | ||
|
|
bb4d1773d3 | ||
|
|
a6c1192bfc | ||
|
|
d14d2fe588 | ||
|
|
f696331563 | ||
|
|
6b2b92a732 | ||
|
|
83ce9450f7 | ||
|
|
0b405c33c4 | ||
|
|
bf74cab7af | ||
|
|
d8adb4bdb0 | ||
|
|
33990badcd | ||
|
|
8061f15aec | ||
|
|
25f7c31911 | ||
|
|
bb98331ba4 | ||
|
|
07d139b3a8 | ||
|
|
f4ef8fd1bc | ||
|
|
ba836c2e36 | ||
|
|
a0ab356936 | ||
|
|
734a83c657 | ||
|
|
b42f4012d1 | ||
|
|
8501312292 | ||
|
|
3faed2edc1 | ||
|
|
bc70619b17 | ||
|
|
bef15264b7 | ||
|
|
6d26915c69 | ||
|
|
fa2e6ada26 | ||
|
|
a6880c452f | ||
|
|
4bccb0d2a1 | ||
|
|
103639455c | ||
|
|
549abd9c7e | ||
|
|
f1aba5511f | ||
|
|
21d05a8b4d | ||
|
|
cb6c869c2f | ||
|
|
640e499964 | ||
|
|
b3b4f7468d | ||
|
|
ad9621ebe5 | ||
|
|
e370d523ec | ||
|
|
61a41bb8fc | ||
|
|
2da6d3c223 | ||
|
|
816efa02d1 | ||
|
|
bd1b1a9ff9 | ||
|
|
1d23f7f900 | ||
|
|
39843a73de | ||
|
|
aec425d1f6 | ||
|
|
855ed2b4e4 | ||
|
|
2dc40fe16e | ||
|
|
bf8376ddcb | ||
|
|
8f696193f0 | ||
|
|
70edb2492a | ||
|
|
e35d4beb95 | ||
|
|
919b431a24 | ||
|
|
7f59a8ea0c | ||
|
|
1ac3f0da63 | ||
|
|
12e679c14d | ||
|
|
28ef94c3fa | ||
|
|
27df4cca6c | ||
|
|
a8413249c2 | ||
|
|
f2dacb2570 | ||
|
|
5aaf81f2c9 | ||
|
|
b86cd325fe | ||
|
|
74b7dabf2d | ||
|
|
1ce4c2092a | ||
|
|
875e05ff38 | ||
|
|
fe0e49db4b | ||
|
|
ad86e68c1e | ||
|
|
e7985c970b | ||
|
|
cfac537f51 | ||
|
|
d6e76969cc | ||
|
|
77dca8272c | ||
|
|
3b8ee196be | ||
|
|
4935043f4a | ||
|
|
f5d74e07d5 | ||
|
|
0a724a5473 | ||
|
|
cba8333a13 | ||
|
|
fcbc399809 | ||
|
|
f6eb9e79d5 | ||
|
|
ab3717af76 | ||
|
|
6cd69b413c | ||
|
|
de56a0d021 | ||
|
|
99fdd3e358 | ||
|
|
9a3107aa66 | ||
|
|
d31e01b877 | ||
|
|
f8c8900297 | ||
|
|
ed9cf994c2 | ||
|
|
d4a4938fce | ||
|
|
f7f0138cff | ||
|
|
753ffdaffd | ||
|
|
40aba3d785 | ||
|
|
64f157a036 | ||
|
|
0eddd287c5 | ||
|
|
f32b50cb80 | ||
|
|
a58a566ae8 | ||
|
|
2f1d40e014 | ||
|
|
14ee6178f9 | ||
|
|
753fe8279b | ||
|
|
cc264f415e | ||
|
|
dae90abb34 | ||
|
|
60f692c7bb | ||
|
|
7094d6d61e | ||
|
|
08fc73aa20 | ||
|
|
c14e41f431 | ||
|
|
f1f4d80f24 | ||
|
|
e746b92e0e | ||
|
|
7d2563eb1f | ||
|
|
084b3287ab | ||
|
|
4105429639 | ||
|
|
8c93b484c4 | ||
|
|
3b38de63ea | ||
|
|
eff1d1f14e | ||
|
|
fcb60d472e | ||
|
|
f2a2f2cca5 | ||
|
|
8c7f0669c6 | ||
|
|
d36c7c3de7 | ||
|
|
79efb0e607 | ||
|
|
9bc26e93a4 | ||
|
|
6c3e2021df | ||
|
|
07255a29b4 | ||
|
|
144bb3492a | ||
|
|
6f4dd7b057 | ||
|
|
27f3285d17 | ||
|
|
9a87e62e0e | ||
|
|
9044a9157f | ||
|
|
799ae894a8 | ||
|
|
bff1e1ff6c | ||
|
|
cc2437614b | ||
|
|
0700886d1a | ||
|
|
cd0e321668 | ||
|
|
94a82ab7dc | ||
|
|
b6e4a7771a | ||
|
|
13859388c1 | ||
|
|
2f4c5f949b | ||
|
|
36e8157268 | ||
|
|
2d88f47795 | ||
|
|
5acfe5da68 | ||
|
|
5f9e4ae136 | ||
|
|
a9b0f92afe | ||
|
|
07b2728380 | ||
|
|
a5e66ce6ba | ||
|
|
eae9726bec | ||
|
|
c425afe50e | ||
|
|
a5b9e59cee | ||
|
|
fb447cab82 | ||
|
|
bcde57bff8 | ||
|
|
dfd7ef1fce | ||
|
|
6b9addfeea | ||
|
|
6c62f7231b | ||
|
|
52c21a53b3 | ||
|
|
fdb250d86c | ||
|
|
8de56cfc10 | ||
|
|
7ea25cd360 | ||
|
|
c9498d9f09 | ||
|
|
2f0435ebd8 | ||
|
|
19351fc429 | ||
|
|
bfc16428da | ||
|
|
41fc44b27c | ||
|
|
a55fbd2be7 | ||
|
|
28d6910e56 | ||
|
|
edfc54b2eb | ||
|
|
6ceafabd78 | ||
|
|
48972c7570 | ||
|
|
bf3ead3359 | ||
|
|
b4f8d52fb1 | ||
|
|
143be49c66 | ||
|
|
a9f19a16ee | ||
|
|
d53a8c0823 | ||
|
|
6e5c541a00 | ||
|
|
2cd127921a | ||
|
|
43d2e436b9 | ||
|
|
ef35b8d428 | ||
|
|
69e86c29a6 | ||
|
|
a4d45c46e8 | ||
|
|
45d1d30a8b | ||
|
|
fa9b9105a8 | ||
|
|
4fb4838bde | ||
|
|
3a487e54a2 | ||
|
|
36da82aa8d | ||
|
|
5205354cb7 | ||
|
|
3498234448 | ||
|
|
c13ebacce1 | ||
|
|
ad49942201 | ||
|
|
82770faad7 | ||
|
|
a2f9fdf339 | ||
|
|
a2decdaaa3 | ||
|
|
72a1b7ae3f | ||
|
|
2753dd0c5e | ||
|
|
118c49ecaa | ||
|
|
0d9b3bea10 | ||
|
|
23afdec767 | ||
|
|
4671bd95c6 | ||
|
|
1bc916927c | ||
|
|
2f8865d6cb | ||
|
|
cfdea8d20f | ||
|
|
6e941af9b2 | ||
|
|
ba9bb90cf7 | ||
|
|
2ff61786bc | ||
|
|
9791c6b21b | ||
|
|
a183043d5d | ||
|
|
0589379de5 | ||
|
|
ee7e59fe68 | ||
|
|
b489519930 | ||
|
|
4395217031 | ||
|
|
c8ad9c4daa | ||
|
|
c050eb4100 | ||
|
|
c8a53c564a | ||
|
|
c316d5b0b9 | ||
|
|
e88fc33eef | ||
|
|
8854efd685 | ||
|
|
b0e850ba5d | ||
|
|
74f1f08ab5 | ||
|
|
aa51bb6cb9 | ||
|
|
8deb462471 | ||
|
|
46dc9322a2 | ||
|
|
daf8143d01 | ||
|
|
54dfe045b2 | ||
|
|
f2dfc84d52 | ||
|
|
e55f7ebf81 | ||
|
|
8d06469efe | ||
|
|
c1127133ea | ||
|
|
25970027c6 | ||
|
|
d7640e6ec3 | ||
|
|
12e76ef7c1 | ||
|
|
d36996c8f0 | ||
|
|
e929f45ab8 | ||
|
|
4c328baaa6 | ||
|
|
cc5edf69e3 | ||
|
|
909f2448ca | ||
|
|
97076aa3fd | ||
|
|
a3777c4ea8 | ||
|
|
1c3293ac85 | ||
|
|
f06a0ba373 | ||
|
|
9afc2634c6 | ||
|
|
ed3efc8712 | ||
|
|
144524fbbb | ||
|
|
298d31e42b | ||
|
|
3e7d4fc902 | ||
|
|
64223cea72 | ||
|
|
1053473111 | ||
|
|
25dcddfeef | ||
|
|
e20f88c143 | ||
|
|
1533a68c06 | ||
|
|
5ff5c73e2b | ||
|
|
6ba49e12a2 | ||
|
|
852ce9f990 | ||
|
|
df69680d24 | ||
|
|
2e7b5dcd19 | ||
|
|
e49e0b5a13 | ||
|
|
de50d5d9c1 | ||
|
|
612a37b2dd | ||
|
|
d47006c98f | ||
|
|
16bf10b1a2 | ||
|
|
710533ae8a | ||
|
|
11c57f9345 | ||
|
|
7562b4164b | ||
|
|
cf44b77225 | ||
|
|
44e9783c7c | ||
|
|
1b5c02ff67 | ||
|
|
2f74ffcf81 | ||
|
|
954e4796b8 | ||
|
|
fb501282cc | ||
|
|
c06351f2a9 | ||
|
|
6b9c65c9ce | ||
|
|
8ae3caa292 | ||
|
|
391e3196ea | ||
|
|
cb709931e4 | ||
|
|
a750f8444e | ||
|
|
a5bff4cd8d | ||
|
|
e0bc894cbb | ||
|
|
3ec56d55c5 | ||
|
|
b904a4e770 | ||
|
|
146a9492ec | ||
|
|
e5d714ef52 | ||
|
|
4d63baf705 | ||
|
|
234bf1f0ea | ||
|
|
843789528e | ||
|
|
ea2c073612 | ||
|
|
d1228d5cf4 | ||
|
|
7aec098a05 | ||
|
|
70af7e5fad | ||
|
|
99e272fc8d | ||
|
|
990f476ac9 | ||
|
|
9abc13aaa6 | ||
|
|
d17186a8b7 | ||
|
|
6fedad7890 | ||
|
|
b371bf700f | ||
|
|
01ce43ec7c | ||
|
|
b903bbc042 | ||
|
|
304137e7ff | ||
|
|
e80628d45b | ||
|
|
d6b81fb345 | ||
|
|
621c653fed | ||
|
|
48d70e520f | ||
|
|
528ad56530 | ||
|
|
be3b227a87 | ||
|
|
ef8fc1f201 | ||
|
|
8fcf085829 | ||
|
|
6843893d9f | ||
|
|
e963fc5acf | ||
|
|
bc664c276c | ||
|
|
f192ef8219 | ||
|
|
db31cdf075 | ||
|
|
f168226be9 | ||
|
|
ea01b127c2 | ||
|
|
6e831138b4 | ||
|
|
eb2671f4bb | ||
|
|
8d017b7678 | ||
|
|
5ec7fc7ddb | ||
|
|
0f3ec94fba | ||
|
|
2c566072f5 | ||
|
|
cf8562a030 | ||
|
|
a91c1bc668 | ||
|
|
d43e6a2888 | ||
|
|
50cea77887 | ||
|
|
6231394614 | ||
|
|
f516cc7dc6 | ||
|
|
9c7523d7b0 | ||
|
|
10505d542a | ||
|
|
e12994a0cd | ||
|
|
ff01aa40c9 | ||
|
|
6199e50e80 | ||
|
|
c664c20165 | ||
|
|
eb551a6d5a | ||
|
|
4343659742 | ||
|
|
230bd3929c | ||
|
|
ba7333e804 | ||
|
|
48b13cc865 | ||
|
|
e7c7b9b2a9 | ||
|
|
c7166241f7 | ||
|
|
6318178a8b | ||
|
|
a2b8ad50f2 | ||
|
|
5c95c53c6c | ||
|
|
e60d066514 | ||
|
|
91fe6e4e56 | ||
|
|
34727be5ac | ||
|
|
107769ab81 | ||
|
|
63cc179ea2 | ||
|
|
2bb1a95098 | ||
|
|
f3411f8db2 | ||
|
|
29e659cf4c |
71
.coveragerc
71
.coveragerc
@@ -4,6 +4,8 @@ source = homeassistant
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/util/async.py
|
||||
homeassistant/monkey_patch.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
@@ -59,6 +61,11 @@ omit =
|
||||
homeassistant/components/coinbase.py
|
||||
homeassistant/components/sensor/coinbase.py
|
||||
|
||||
homeassistant/components/cast/*
|
||||
homeassistant/components/*/cast.py
|
||||
|
||||
homeassistant/components/cloudflare.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
@@ -95,7 +102,7 @@ omit =
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
homeassistant/components/switch/fritzbox.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
@@ -121,13 +128,16 @@ omit =
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/hydrawise.py
|
||||
homeassistant/components/*/hydrawise.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
homeassistant/components/insteon_plm/*
|
||||
homeassistant/components/*/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
@@ -151,6 +161,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/konnected.py
|
||||
homeassistant/components/*/konnected.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
@@ -181,18 +194,21 @@ omit =
|
||||
homeassistant/components/mychevy.py
|
||||
homeassistant/components/*/mychevy.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/mysensors/*
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/nest/__init__.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/netgear_lte.py
|
||||
homeassistant/components/*/netgear_lte.py
|
||||
|
||||
homeassistant/components/octoprint.py
|
||||
homeassistant/components/*/octoprint.py
|
||||
|
||||
@@ -211,7 +227,7 @@ omit =
|
||||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine.py
|
||||
homeassistant/components/rainmachine/*
|
||||
homeassistant/components/*/rainmachine.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
@@ -226,18 +242,27 @@ omit =
|
||||
homeassistant/components/rpi_pfio.py
|
||||
homeassistant/components/*/rpi_pfio.py
|
||||
|
||||
homeassistant/components/sabnzbd.py
|
||||
homeassistant/components/*/sabnzbd.py
|
||||
|
||||
homeassistant/components/satel_integra.py
|
||||
homeassistant/components/*/satel_integra.py
|
||||
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
|
||||
homeassistant/components/skybell.py
|
||||
homeassistant/components/*/skybell.py
|
||||
|
||||
homeassistant/components/smappee.py
|
||||
homeassistant/components/*/smappee.py
|
||||
|
||||
homeassistant/components/sonos/__init__.py
|
||||
homeassistant/components/*/sonos.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
@@ -300,6 +325,9 @@ omit =
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/wirelesstag.py
|
||||
homeassistant/components/*/wirelesstag.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
homeassistant/components/*/xiaomi_aqara.py
|
||||
|
||||
@@ -318,6 +346,12 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/tuya.py
|
||||
homeassistant/components/*/tuya.py
|
||||
|
||||
homeassistant/components/spider.py
|
||||
homeassistant/components/*/spider.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -337,11 +371,13 @@ omit =
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/binary_sensor/uptimerobot.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/caldav.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/familyhub.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@@ -351,6 +387,7 @@ omit =
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/xeoma.py
|
||||
homeassistant/components/camera/xiaomi.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
@@ -366,6 +403,9 @@ omit =
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/climate/zhong_hong.py
|
||||
homeassistant/components/cover/aladdin_connect.py
|
||||
homeassistant/components/cover/brunt.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/gogogate2.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
@@ -373,6 +413,7 @@ omit =
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/ryobi_gdo.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
@@ -384,6 +425,7 @@ omit =
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/freebox.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
@@ -412,7 +454,6 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
@@ -428,6 +469,7 @@ omit =
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/futurenow.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
@@ -435,6 +477,7 @@ omit =
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/lw12wifi.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
@@ -449,6 +492,7 @@ omit =
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/kiwi.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
@@ -459,7 +503,6 @@ omit =
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/bluesound.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/channels.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
@@ -468,10 +511,12 @@ omit =
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/epson.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/horizon.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@@ -493,7 +538,6 @@ omit =
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/songpal.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/ue_smart_radio.py
|
||||
@@ -510,9 +554,10 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/flock.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
@@ -525,7 +570,6 @@ omit =
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/prowl.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
@@ -582,6 +626,7 @@ omit =
|
||||
homeassistant/components/sensor/domain_expiry.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/duke_energy.py
|
||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
@@ -592,6 +637,7 @@ omit =
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/filesize.py
|
||||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
@@ -611,6 +657,7 @@ omit =
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/iperf3.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
@@ -619,6 +666,7 @@ omit =
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/magicseaweed.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mitemp_bt.py
|
||||
@@ -629,6 +677,7 @@ omit =
|
||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nsw_fuel_station.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
@@ -650,7 +699,6 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
@@ -734,6 +782,7 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/watson_iot.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed:
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -107,3 +107,6 @@ desktop.ini
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
||||
# monkeytype
|
||||
monkeytype.sqlite3
|
||||
|
||||
2
.isort.cfg
Normal file
2
.isort.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
20
.travis.yml
20
.travis.yml
@@ -10,17 +10,23 @@ matrix:
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
dist: xenial
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
dist: xenial
|
||||
if: branch = dev AND type = push
|
||||
allow_failures:
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
dist: xenial
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
@@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
@@ -78,7 +79,6 @@ homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/upnp.py @dgomes
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
@@ -94,10 +94,14 @@ homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/*/rainmachine.py @bachya
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to Home Assistant
|
||||
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spend a couple of hours and help to integrate them?
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_SSOCR no
|
||||
#ENV INSTALL_IPERF3 no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.1"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
# array of all schemes that your API supports
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
#api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: api_password
|
||||
# in: query
|
||||
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: x-ha-access
|
||||
in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/config:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
schema:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/discovery_info:
|
||||
get:
|
||||
summary: Basic information about Home Assistant instance
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Basic information
|
||||
schema:
|
||||
$ref: '#/definitions/DiscoveryInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/bootstrap:
|
||||
get:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
schema:
|
||||
$ref: '#/definitions/BootstrapInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events:
|
||||
get:
|
||||
summary: Array of event objects.
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services:
|
||||
get:
|
||||
summary: Array of service objects.
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/history:
|
||||
get:
|
||||
summary: Array of state changes in the past.
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/History'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states:
|
||||
get:
|
||||
summary: Array of state objects.
|
||||
description: |
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states/{entity_id}:
|
||||
get:
|
||||
summary: Specific state object.
|
||||
description: |
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the entity to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: State
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
404:
|
||||
description: Not found
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
post:
|
||||
description: |
|
||||
Updates or creates the current state of an entity.
|
||||
tags:
|
||||
- State
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id to set the state of
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/State'
|
||||
responses:
|
||||
200:
|
||||
description: State of existing entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
201:
|
||||
description: State of new entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
headers:
|
||||
location:
|
||||
type: string
|
||||
description: location of the new entity
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/error_log:
|
||||
get:
|
||||
summary: Error log
|
||||
description: |
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
200:
|
||||
description: Plain text error log
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/camera_proxy/camera.{entity_id}:
|
||||
get:
|
||||
summary: Camera image.
|
||||
description: |
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the camera to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Camera image
|
||||
schema:
|
||||
type: file
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events/{event_type}:
|
||||
post:
|
||||
description: |
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: event_type
|
||||
in: path
|
||||
description: event_type to fire event with
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/EventData'
|
||||
responses:
|
||||
200:
|
||||
description: Response message
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services/{domain}/{service}:
|
||||
post:
|
||||
description: |
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
description: domain of the service
|
||||
required: true
|
||||
type: string
|
||||
- name: service
|
||||
in: path
|
||||
description: service to call
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/ServiceData'
|
||||
responses:
|
||||
200:
|
||||
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/template:
|
||||
post:
|
||||
description: |
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- text/plain
|
||||
parameters:
|
||||
- $ref: '#/parameters/Template'
|
||||
responses:
|
||||
200:
|
||||
description: Returns the rendered template in plain text.
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/event_forwarding:
|
||||
post:
|
||||
description: |
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was setup successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
delete:
|
||||
description: |
|
||||
Cancel event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was cancelled successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/stream:
|
||||
get:
|
||||
summary: Server-sent events
|
||||
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
- name: restrict
|
||||
in: query
|
||||
description: comma-separated list of event_types to filter
|
||||
required: false
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: Stream of events
|
||||
schema:
|
||||
type: object
|
||||
x-events:
|
||||
state_changed:
|
||||
type: object
|
||||
properties:
|
||||
entity_id:
|
||||
type: string
|
||||
old_state:
|
||||
$ref: '#/definitions/State'
|
||||
new_state:
|
||||
$ref: '#/definitions/State'
|
||||
definitions:
|
||||
ApiConfig:
|
||||
type: object
|
||||
properties:
|
||||
components:
|
||||
type: array
|
||||
description: List of component types
|
||||
items:
|
||||
type: string
|
||||
description: Component type
|
||||
latitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Latitude of Home Assistant server
|
||||
longitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Longitude of Home Assistant server
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: string
|
||||
mass:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
DiscoveryInfo:
|
||||
type: object
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
location_name:
|
||||
type: string
|
||||
requires_api_password:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
BootstrapInfo:
|
||||
type: object
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
listener_count:
|
||||
type: integer
|
||||
Service:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
services:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DomainService'
|
||||
DomainService:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
type: object
|
||||
description: Object with service fields that can be called
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
entity_id:
|
||||
type: string
|
||||
last_changed:
|
||||
type: string
|
||||
format: date-time
|
||||
StateAttributes:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
History:
|
||||
allOf:
|
||||
- $ref: '#/definitions/State'
|
||||
- type: object
|
||||
properties:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
parameters:
|
||||
State:
|
||||
name: body
|
||||
in: body
|
||||
description: State parameter
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- state
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
EventData:
|
||||
name: body
|
||||
in: body
|
||||
description: event_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
ServiceData:
|
||||
name: body
|
||||
in: body
|
||||
description: service_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
Template:
|
||||
name: body
|
||||
in: body
|
||||
description: Template to render
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- template
|
||||
properties:
|
||||
template:
|
||||
description: Jinja2 template string
|
||||
type: string
|
||||
EventForwarding:
|
||||
name: body
|
||||
in: body
|
||||
description: Event Forwarding parameter
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- host
|
||||
- api_password
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
api_password:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
@@ -8,7 +8,8 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
||||
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
@@ -19,7 +20,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
|
||||
def attempt_use_uvloop():
|
||||
def attempt_use_uvloop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
import asyncio
|
||||
|
||||
@@ -240,7 +241,7 @@ def cmdline() -> List[str]:
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> Optional[int]:
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
@@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
@@ -273,17 +274,17 @@ def setup_and_run_hass(config_dir: str,
|
||||
log_no_color=args.log_no_color)
|
||||
|
||||
if hass is None:
|
||||
return None
|
||||
return -1
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
def open_browser(event):
|
||||
"""Open the webinterface in a browser."""
|
||||
if hass.config.api is not None:
|
||||
def open_browser(_: Any) -> None:
|
||||
"""Open the web interface in a browser."""
|
||||
if hass.config.api is not None: # type: ignore
|
||||
import webbrowser
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||
|
||||
run_callback_threadsafe(
|
||||
hass.loop,
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import binascii
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import importlib
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
DATA_REQS = 'auth_reqs_processed'
|
||||
|
||||
|
||||
class AuthError(HomeAssistantError):
|
||||
"""Generic authentication error."""
|
||||
|
||||
|
||||
class InvalidUser(AuthError):
|
||||
"""Raised when an invalid user has been specified."""
|
||||
|
||||
|
||||
class InvalidPassword(AuthError):
|
||||
"""Raised when an invalid password has been supplied."""
|
||||
|
||||
|
||||
class UnknownError(AuthError):
|
||||
"""When an unknown error occurs."""
|
||||
|
||||
|
||||
def generate_secret(entropy=32):
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
initialized = False
|
||||
|
||||
def __init__(self, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
return await self.store.credentials_for_provider(self.type, self.id)
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the auth provider.
|
||||
|
||||
Optional.
|
||||
"""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
name = attr.ib(type=str, default=None)
|
||||
# For persisting and see if saved?
|
||||
# store = attr.ib(type=AuthStore, default=None)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
|
||||
|
||||
def as_dict(self):
|
||||
"""Convert user object to a dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'is_owner': self.is_owner,
|
||||
'is_active': self.is_active,
|
||||
'name': self.name,
|
||||
}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
access_token_expiration = attr.ib(type=timedelta,
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expires(self):
|
||||
"""Return datetime when this token expires."""
|
||||
return self.created_at + self.refresh_token.access_token_expiration
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str)
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Client:
|
||||
"""Client that interacts with Home Assistant on behalf of a user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth_providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
return module
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
"""Initialize an auth manager from config."""
|
||||
store = AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[_auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
return manager
|
||||
|
||||
|
||||
async def _auth_provider_from_config(hass, store, config):
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](store, config)
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
"""Initialize the auth manager."""
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self.access_tokens = {}
|
||||
|
||||
@property
|
||||
def async_auth_providers(self):
|
||||
"""Return a list of available auth providers."""
|
||||
return self._providers.values()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
"""Get or create a user."""
|
||||
return await self._store.async_get_or_create_user(
|
||||
credentials, self._async_get_auth_provider(credentials))
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new refresh token for a user."""
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = AccessToken(refresh_token)
|
||||
self.access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
return self.access_tokens.get(token)
|
||||
|
||||
async def async_create_client(self, name):
|
||||
"""Create a new client."""
|
||||
return await self._store.async_create_client(name)
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
return await self._store.async_get_client(client_id)
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
if not auth_provider.initialized:
|
||||
auth_provider.initialized = True
|
||||
await auth_provider.async_initialize()
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers[auth_provider_key]
|
||||
|
||||
|
||||
class AuthStore:
|
||||
"""Stores authentication info.
|
||||
|
||||
Any mutation to an object should happen inside the auth store.
|
||||
|
||||
The auth store is lazy. It won't load the data from disk until a method is
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self.users = None
|
||||
self.clients = None
|
||||
self._load_lock = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
async def credentials_for_provider(self, provider_type, provider_id):
|
||||
"""Return credentials for specific auth provider type and id."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return [
|
||||
credentials
|
||||
for user in self.users.values()
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == provider_type and
|
||||
credentials.auth_provider_id == provider_id)
|
||||
]
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.users.get(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials, auth_provider):
|
||||
"""Get or create a new user for given credentials.
|
||||
|
||||
If link_user is passed in, the credentials will be linked to the passed
|
||||
in user if the credentials are new.
|
||||
"""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
# New credentials, store in user
|
||||
if credentials.is_new:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
# Make owner and activate user if it's the first user.
|
||||
if self.users:
|
||||
is_owner = False
|
||||
is_active = False
|
||||
else:
|
||||
is_owner = True
|
||||
is_active = True
|
||||
|
||||
new_user = User(
|
||||
is_owner=is_owner,
|
||||
is_active=is_active,
|
||||
name=info.get('name'),
|
||||
)
|
||||
self.users[new_user.id] = new_user
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
for user in self.users.values():
|
||||
for creds in user.credentials:
|
||||
if (creds.auth_provider_type == credentials.auth_provider_type
|
||||
and creds.auth_provider_id ==
|
||||
credentials.auth_provider_id):
|
||||
return user
|
||||
|
||||
raise ValueError('We got credentials with ID but found no user')
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self.users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = RefreshToken(user, client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self.users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_client(self, name):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
client = Client(name)
|
||||
self.clients[client.id] = client
|
||||
await self.async_save()
|
||||
return client
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def async_load(self):
|
||||
"""Load the users."""
|
||||
async with self._load_lock:
|
||||
self.users = {}
|
||||
self.clients = {}
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
pass
|
||||
243
homeassistant/auth/__init__.py
Normal file
243
homeassistant/auth/__init__.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import models
|
||||
from . import auth_store
|
||||
from .providers import auth_provider_from_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
"""Initialize an auth manager from config."""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
return manager
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
"""Initialize the auth manager."""
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self._access_tokens = OrderedDict()
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
"""Return if any auth providers are registered."""
|
||||
return bool(self._providers)
|
||||
|
||||
@property
|
||||
def support_legacy(self):
|
||||
"""
|
||||
Return if legacy_api_password auth providers are registered.
|
||||
|
||||
Should be removed when we removed legacy_api_password auth providers.
|
||||
"""
|
||||
for provider_type, _ in self._providers:
|
||||
if provider_type == 'legacy_api_password':
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def auth_providers(self):
|
||||
"""Return a list of available auth providers."""
|
||||
return list(self._providers.values())
|
||||
|
||||
async def async_get_users(self):
|
||||
"""Retrieve all users."""
|
||||
return await self._store.async_get_users()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_create_system_user(self, name):
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
async def async_create_user(self, name):
|
||||
"""Create a user."""
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
"""Get or create a user."""
|
||||
if not credentials.is_new:
|
||||
for user in await self._store.async_get_users():
|
||||
for creds in user.credentials:
|
||||
if creds.id == credentials.id:
|
||||
return user
|
||||
|
||||
raise ValueError('Unable to find the user.')
|
||||
|
||||
auth_provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if auth_provider is None:
|
||||
raise RuntimeError('Credential with unknown provider encountered')
|
||||
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
|
||||
return await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.get('name'),
|
||||
is_active=info.get('is_active', False)
|
||||
)
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
tasks = [
|
||||
self.async_remove_credentials(credentials)
|
||||
for credentials in user.credentials
|
||||
]
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
"""Deactivate a user."""
|
||||
if user.is_owner:
|
||||
raise ValueError('Unable to deactive the owner')
|
||||
await self._store.async_deactivate_user(user)
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
"""Remove credentials."""
|
||||
provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if (provider is not None and
|
||||
hasattr(provider, 'async_will_remove_credentials')):
|
||||
await provider.async_will_remove_credentials(credentials)
|
||||
|
||||
await self._store.async_remove_credentials(credentials)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
raise ValueError('User is not active')
|
||||
|
||||
if user.system_generated and client_id is not None:
|
||||
raise ValueError(
|
||||
'System generated users cannot have refresh tokens connected '
|
||||
'to a client.')
|
||||
|
||||
if not user.system_generated and client_id is None:
|
||||
raise ValueError('Client is required to generate a refresh token.')
|
||||
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = models.AccessToken(refresh_token=refresh_token)
|
||||
self._access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
tkn = self._access_tokens.get(token)
|
||||
|
||||
if tkn is None:
|
||||
_LOGGER.debug('Attempt to get non-existing access token')
|
||||
return None
|
||||
|
||||
if tkn.expired or not tkn.refresh_token.user.is_active:
|
||||
if tkn.expired:
|
||||
_LOGGER.debug('Attempt to get expired access token')
|
||||
else:
|
||||
_LOGGER.debug('Attempt to get access token for inactive user')
|
||||
self._access_tokens.pop(token)
|
||||
return None
|
||||
|
||||
return tkn
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers.get(auth_provider_key)
|
||||
|
||||
async def _user_should_be_owner(self):
|
||||
"""Determine if user should be owner.
|
||||
|
||||
A user should be an owner if it is the first non-system user that is
|
||||
being created.
|
||||
"""
|
||||
for user in await self._store.async_get_users():
|
||||
if not user.system_generated:
|
||||
return False
|
||||
|
||||
return True
|
||||
240
homeassistant/auth/auth_store.py
Normal file
240
homeassistant/auth/auth_store.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Storage for auth models."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
"""Stores authentication info.
|
||||
|
||||
Any mutation to an object should happen inside the auth store.
|
||||
|
||||
The auth store is lazy. It won't load the data from disk until a method is
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
async def async_get_users(self):
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
return list(self._users.values())
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user by id."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
return self._users.get(user_id)
|
||||
|
||||
async def async_create_user(self, name, is_owner=None, is_active=None,
|
||||
system_generated=None, credentials=None):
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
if is_owner is not None:
|
||||
kwargs['is_owner'] = is_owner
|
||||
|
||||
if is_active is not None:
|
||||
kwargs['is_active'] = is_active
|
||||
|
||||
if system_generated is not None:
|
||||
kwargs['system_generated'] = system_generated
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
self._users[new_user.id] = new_user
|
||||
|
||||
if credentials is None:
|
||||
await self.async_save()
|
||||
return new_user
|
||||
|
||||
# Saving is done inside the link.
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self._users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
"""Activate a user."""
|
||||
user.is_active = True
|
||||
await self.async_save()
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
"""Activate a user."""
|
||||
user.is_active = False
|
||||
await self.async_save()
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
"""Remove credentials."""
|
||||
for user in self._users.values():
|
||||
found = None
|
||||
|
||||
for index, cred in enumerate(user.credentials):
|
||||
if cred is credentials:
|
||||
found = index
|
||||
break
|
||||
|
||||
if found is not None:
|
||||
user.credentials.pop(found)
|
||||
break
|
||||
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = models.RefreshToken(user=user, client_id=client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_load(self):
|
||||
"""Load the users."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
# Make sure that we're not overriding data if 2 loads happened at the
|
||||
# same time
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
users = OrderedDict()
|
||||
|
||||
if data is None:
|
||||
self._users = users
|
||||
return
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(**user_dict)
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
id=cred_dict['id'],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict['auth_provider_type'],
|
||||
auth_provider_id=cred_dict['auth_provider_id'],
|
||||
data=cred_dict['data'],
|
||||
))
|
||||
|
||||
refresh_tokens = OrderedDict()
|
||||
|
||||
for rt_dict in data['refresh_tokens']:
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict['id'],
|
||||
user=users[rt_dict['user_id']],
|
||||
client_id=rt_dict['client_id'],
|
||||
created_at=dt_util.parse_datetime(rt_dict['created_at']),
|
||||
access_token_expiration=timedelta(
|
||||
seconds=rt_dict['access_token_expiration']),
|
||||
token=rt_dict['token'],
|
||||
)
|
||||
refresh_tokens[token.id] = token
|
||||
users[rt_dict['user_id']].refresh_tokens[token.token] = token
|
||||
|
||||
for ac_dict in data['access_tokens']:
|
||||
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
|
||||
token = models.AccessToken(
|
||||
refresh_token=refresh_token,
|
||||
created_at=dt_util.parse_datetime(ac_dict['created_at']),
|
||||
token=ac_dict['token'],
|
||||
)
|
||||
refresh_token.access_tokens.append(token)
|
||||
|
||||
self._users = users
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
'system_generated': user.system_generated,
|
||||
}
|
||||
for user in self._users.values()
|
||||
]
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
'user_id': user.id,
|
||||
'auth_provider_type': credential.auth_provider_type,
|
||||
'auth_provider_id': credential.auth_provider_id,
|
||||
'data': credential.data,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for credential in user.credentials
|
||||
]
|
||||
|
||||
refresh_tokens = [
|
||||
{
|
||||
'id': refresh_token.id,
|
||||
'user_id': user.id,
|
||||
'client_id': refresh_token.client_id,
|
||||
'created_at': refresh_token.created_at.isoformat(),
|
||||
'access_token_expiration':
|
||||
refresh_token.access_token_expiration.total_seconds(),
|
||||
'token': refresh_token.token,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
]
|
||||
|
||||
access_tokens = [
|
||||
{
|
||||
'id': user.id,
|
||||
'refresh_token_id': refresh_token.id,
|
||||
'created_at': access_token.created_at.isoformat(),
|
||||
'token': access_token.token,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
for access_token in refresh_token.access_tokens
|
||||
]
|
||||
|
||||
data = {
|
||||
'users': users,
|
||||
'credentials': credentials,
|
||||
'access_tokens': access_tokens,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
||||
await self._store.async_save(data, delay=1)
|
||||
4
homeassistant/auth/const.py
Normal file
4
homeassistant/auth/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the auth module."""
|
||||
from datetime import timedelta
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
75
homeassistant/auth/models.py
Normal file
75
homeassistant/auth/models.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Auth models."""
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ACCESS_TOKEN_EXPIRATION
|
||||
from .util import generate_secret
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
access_token_expiration = attr.ib(type=timedelta,
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
"""Return if this token has expired."""
|
||||
expires = self.created_at + self.refresh_token.access_token_expiration
|
||||
return dt_util.utcnow() > expires
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str)
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
143
homeassistant/auth/providers/__init__.py
Normal file
143
homeassistant/auth/providers/__init__.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from homeassistant.auth.models import Credentials
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = 'auth_prov_reqs_processed'
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def auth_provider_from_config(hass, store, config):
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth.providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
processed.add(provider)
|
||||
return module
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
users = await self.store.async_get_users()
|
||||
return [
|
||||
credentials
|
||||
for user in users
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == self.type and
|
||||
credentials.auth_provider_id == self.id)
|
||||
]
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
|
||||
Values to populate:
|
||||
- name: string
|
||||
- is_active: boolean
|
||||
"""
|
||||
return {}
|
||||
238
homeassistant/auth/providers/homeassistant.py
Normal file
238
homeassistant/auth/providers/homeassistant.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Home Assistant auth provider."""
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Dict # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from homeassistant.auth.util import generate_secret
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
||||
|
||||
|
||||
def _disallow_id(conf):
|
||||
"""Disallow ID in config."""
|
||||
if CONF_ID in conf:
|
||||
raise vol.Invalid(
|
||||
'ID is not allowed for the homeassistant auth provider.')
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Raised when we encounter invalid authentication."""
|
||||
|
||||
|
||||
class InvalidUser(HomeAssistantError):
|
||||
"""Raised when invalid user is specified.
|
||||
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the user data store."""
|
||||
self.hass = hass
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._data = None
|
||||
|
||||
async def async_load(self):
|
||||
"""Load stored data."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Return users."""
|
||||
return self._data['users']
|
||||
|
||||
def validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
hashed = self.hash_password(password)
|
||||
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self._data['users']:
|
||||
if username == user['username']:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(hashed, hashed)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(hashed,
|
||||
base64.b64decode(found['password'])):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed = hashlib.pbkdf2_hmac(
|
||||
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
'username': username,
|
||||
'password': self.hash_password(password, True).decode(),
|
||||
})
|
||||
|
||||
@callback
|
||||
def async_remove_auth(self, username: str) -> None:
|
||||
"""Remove authentication."""
|
||||
index = None
|
||||
for i, user in enumerate(self.users):
|
||||
if user['username'] == username:
|
||||
index = i
|
||||
break
|
||||
|
||||
if index is None:
|
||||
raise InvalidUser
|
||||
|
||||
self.users.pop(index)
|
||||
|
||||
def change_password(self, username: str, new_password: str) -> None:
|
||||
"""Update the password.
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
user['password'] = self.hash_password(
|
||||
new_password, True).decode()
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
async def async_save(self):
|
||||
"""Save data."""
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('homeassistant')
|
||||
class HassAuthProvider(AuthProvider):
|
||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||
|
||||
DEFAULT_TITLE = 'Home Assistant Local'
|
||||
|
||||
data = None
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the auth provider."""
|
||||
if self.data is not None:
|
||||
return
|
||||
|
||||
self.data = Data(self.hass)
|
||||
await self.data.async_load()
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username: str, password: str):
|
||||
"""Helper to validate a username and password."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.validate_login, username, password)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Get extra info for this credential."""
|
||||
return {
|
||||
'name': credentials.data['username'],
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
async def async_will_remove_credentials(self, credentials):
|
||||
"""When credentials get removed, also remove the auth."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
|
||||
try:
|
||||
self.data.async_remove_auth(credentials.data['username'])
|
||||
await self.data.async_save()
|
||||
except InvalidUser:
|
||||
# Can happen if somehow we didn't clean up a credential
|
||||
pass
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except InvalidAuth:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -4,9 +4,12 @@ import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
@@ -15,13 +18,17 @@ USER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
vol.Required('users'): [USER_SCHEMA]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(auth.AuthProvider):
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
@@ -43,18 +50,15 @@ class ExampleAuthProvider(auth.AuthProvider):
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password.encode('utf-8'),
|
||||
password.encode('utf-8'))
|
||||
raise auth.InvalidUser
|
||||
raise InvalidAuthError
|
||||
|
||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise auth.InvalidPassword
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
password = flow_result['password']
|
||||
|
||||
self.async_validate_login(username, password)
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
@@ -71,14 +75,16 @@ class ExampleAuthProvider(auth.AuthProvider):
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data['username']
|
||||
info = {
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
for user in self.config['users']:
|
||||
if user['username'] == username:
|
||||
return {
|
||||
'name': user.get('name')
|
||||
}
|
||||
info['name'] = user.get('name')
|
||||
break
|
||||
|
||||
return {}
|
||||
return info
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
@@ -96,7 +102,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except (auth.InvalidUser, auth.InvalidPassword):
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
110
homeassistant/auth/providers/legacy_api_password.py
Normal file
110
homeassistant/auth/providers/legacy_api_password.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Support Legacy API password auth provider.
|
||||
|
||||
It will be removed when auth system production ready
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
LEGACY_USER = 'homeassistant'
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
DEFAULT_TITLE = 'Legacy API Password'
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, password):
|
||||
"""Helper to validate a username and password."""
|
||||
if not hasattr(self.hass, 'http'):
|
||||
raise ValueError('http component is not loaded')
|
||||
|
||||
if self.hass.http.api_password is None:
|
||||
raise ValueError('http component is not configured using'
|
||||
' api_password')
|
||||
|
||||
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Return LEGACY_USER always."""
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == LEGACY_USER:
|
||||
return credential
|
||||
|
||||
return self.async_create_credentials({
|
||||
'username': LEGACY_USER
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""
|
||||
Set name as LEGACY_USER always.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {
|
||||
'name': LEGACY_USER,
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['password'])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data={}
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
13
homeassistant/auth/util.py
Normal file
13
homeassistant/auth/util.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Auth utils."""
|
||||
import binascii
|
||||
import os
|
||||
|
||||
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
@@ -1 +0,0 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
@@ -17,7 +16,7 @@ from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
@@ -29,9 +28,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
|
||||
'introduction', 'frontend', 'history'))
|
||||
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
|
||||
'logger', 'introduction', 'frontend', 'history'}
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -53,8 +51,9 @@ def from_config_dict(config: Dict[str, Any],
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir, hass.loop))
|
||||
if not is_virtual_env():
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
@@ -95,7 +94,8 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
await hass.async_add_executor_job(
|
||||
conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
@@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
components.update(hass.config_entries.async_domains())
|
||||
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
@@ -138,7 +137,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
hass.async_create_task(async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -146,7 +145,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
hass.async_create_task(async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -163,7 +162,8 @@ def from_config_file(config_path: str,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -188,7 +188,8 @@ async def async_from_config_file(config_path: str,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
@@ -197,13 +198,15 @@ async def async_from_config_file(config_path: str,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
await async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(config_dir)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
try:
|
||||
config_dict = await hass.async_add_job(
|
||||
config_dict = await hass.async_add_executor_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
@@ -211,16 +214,15 @@ async def async_from_config_file(config_path: str,
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
hass = await async_from_config_dict(
|
||||
return await async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
return hass
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days=None,
|
||||
log_file=None,
|
||||
log_rotate_days: Optional[int] = None,
|
||||
log_file: Optional[str] = None,
|
||||
log_no_color: bool = False) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
@@ -278,7 +280,8 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
err_log_path, when='midnight', backupCount=log_rotate_days)
|
||||
err_log_path, when='midnight',
|
||||
backupCount=log_rotate_days) # type: logging.FileHandler
|
||||
else:
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
@@ -288,16 +291,16 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
|
||||
async def async_stop_async_handler(event):
|
||||
async def async_stop_async_handler(_: Any) -> None:
|
||||
"""Cleanup async handler."""
|
||||
logging.getLogger('').removeHandler(async_handler)
|
||||
logging.getLogger('').removeHandler(async_handler) # type: ignore
|
||||
await async_handler.async_close(blocking=True)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(async_handler)
|
||||
logger.addHandler(async_handler) # type: ignore
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
@@ -307,23 +310,13 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = await async_get_user_site(deps_dir, loop=loop)
|
||||
lib_dir = await async_get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -167,7 +167,7 @@ def async_setup(hass, config):
|
||||
def async_handle_core_service(call):
|
||||
"""Service handler for handling core services."""
|
||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||
hass.async_add_job(hass.async_stop())
|
||||
hass.async_create_task(hass.async_stop())
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -183,7 +183,7 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
|
||||
@@ -85,7 +85,7 @@ ABODE_PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
class AbodeSystem(object):
|
||||
class AbodeSystem:
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, cache,
|
||||
|
||||
@@ -110,7 +110,7 @@ NotificationItem = namedtuple(
|
||||
)
|
||||
|
||||
|
||||
class AdsHub(object):
|
||||
class AdsHub:
|
||||
"""Representation of an ADS connection."""
|
||||
|
||||
def __init__(self, ads_client):
|
||||
|
||||
@@ -121,7 +121,7 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
@@ -154,6 +154,16 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
@@ -177,7 +187,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_disarm, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_disarm, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -188,7 +198,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_home, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_home, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@@ -199,7 +209,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_away, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_away, code)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
@@ -210,7 +220,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_night, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_night, code)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
@@ -221,7 +231,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_trigger, code)
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
@@ -232,7 +242,8 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
|
||||
return self.hass.async_add_executor_job(
|
||||
self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
||||
@@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -79,17 +80,21 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state.lower() == 'armed stay':
|
||||
if self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_UNKNOWN
|
||||
|
||||
|
||||
@@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.arlo import (
|
||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
@@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
data = hass.data[DATA_ARLO]
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
if not data.base_stations:
|
||||
if not arlo.base_stations:
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
@@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
# PyArlo sometimes returns None for mode. So retry 3 times before
|
||||
# returning None.
|
||||
num_retries = 3
|
||||
i = 0
|
||||
while i < num_retries:
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
return
|
||||
i += 1
|
||||
self._state = None
|
||||
_LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._base_station.mode = DISARMED
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
self._base_station.mode = self._away_mode_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
self._base_station.mode = self._home_mode_name
|
||||
|
||||
@@ -119,10 +122,10 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Convert Arlo mode to Home Assistant state."""
|
||||
if mode == ARMED:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode == DISARMED:
|
||||
if mode == DISARMED:
|
||||
return STATE_ALARM_DISARMED
|
||||
elif mode == self._home_mode_name:
|
||||
if mode == self._home_mode_name:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
if mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
return mode
|
||||
|
||||
@@ -55,9 +55,9 @@ class CanaryAlarm(AlarmControlPanel):
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode.name == LOCATION_MODE_HOME:
|
||||
if mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
if mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
return None
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.components.alarm_control_panel import manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
|
||||
@@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return '^\\d{4,6}$'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Support for HomematicIP alarm control panel.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||
HMIP_ZONE_HOME = 'INTERNAL'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP alarm control devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP alarm control panel from a config entry."""
|
||||
from homematicip.aio.group import AsyncSecurityZoneGroup
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for group in home.groups:
|
||||
if isinstance(group, AsyncSecurityZoneGroup):
|
||||
devices.append(HomematicipSecurityZone(home, group))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
"""Representation of an HomematicIP security zone group."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the security zone group."""
|
||||
device.modelType = 'Group-SecurityZone'
|
||||
device.windowState = ''
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from homematicip.base.enums import WindowState
|
||||
|
||||
if self._device.active:
|
||||
if (self._device.sabotage or self._device.motionDetected or
|
||||
self._device.windowState == WindowState.OPEN):
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
active = self._home.get_security_zones_activation()
|
||||
if active == (True, True):
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
if active == (False, True):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
await self._home.set_security_zones_activation(False, False)
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
await self._home.set_security_zones_activation(True, False)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
await self._home.set_security_zones_activation(True, True)
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ifttt/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -8,6 +8,7 @@ import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -18,7 +19,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback
|
||||
@@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -6,12 +6,13 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
@@ -19,7 +20,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability)
|
||||
CONF_RETAIN, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -48,11 +49,15 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
@@ -65,9 +70,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@@ -76,6 +81,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
@@ -117,8 +123,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
@@ -129,7 +139,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos)
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
@@ -140,7 +151,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos)
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
@@ -151,7 +163,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos)
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos,
|
||||
self._retain)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
||||
@@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return che characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -5,26 +5,26 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.5']
|
||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DOMAIN = 'simplisafe'
|
||||
|
||||
NOTIFICATION_ID = 'simplisafe_notification'
|
||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
@@ -36,36 +36,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, get_systems
|
||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
simplisafe = SimpliSafeApiInterface()
|
||||
status = simplisafe.set_credentials(username, password)
|
||||
if status:
|
||||
hass.data[DOMAIN] = simplisafe
|
||||
locations = get_systems(simplisafe)
|
||||
for location in locations:
|
||||
add_devices([SimpliSafeAlarm(location, name, code)])
|
||||
else:
|
||||
message = 'Failed to log into SimpliSafe. Check credentials.'
|
||||
_LOGGER.error(message)
|
||||
hass.components.persistent_notification.create(
|
||||
message,
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
try:
|
||||
simplisafe = SimpliSafeApiInterface(username, password)
|
||||
except SimpliSafeAPIException:
|
||||
_LOGGER.error("Failed to setup SimpliSafe")
|
||||
return
|
||||
|
||||
def logout(event):
|
||||
"""Logout of the SimpliSafe API."""
|
||||
hass.data[DOMAIN].logout()
|
||||
systems = []
|
||||
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout)
|
||||
for system in simplisafe.get_systems():
|
||||
systems.append(SimpliSafeAlarm(system, name, code))
|
||||
|
||||
add_devices(systems)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, simplisafe, name, code):
|
||||
@@ -74,27 +65,37 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self.simplisafe.location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
status = self.simplisafe.state()
|
||||
if status == 'off':
|
||||
status = self.simplisafe.state
|
||||
if status.lower() == 'off':
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == 'home':
|
||||
elif status.lower() == 'home' or status.lower() == 'home_count':
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'away':
|
||||
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
|
||||
status.lower() == 'away_count'):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
@@ -103,14 +104,13 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'alarm': self.simplisafe.alarm(),
|
||||
'co': self.simplisafe.carbon_monoxide(),
|
||||
'fire': self.simplisafe.fire(),
|
||||
'flood': self.simplisafe.flood(),
|
||||
'last_event': self.simplisafe.last_event(),
|
||||
'temperature': self.simplisafe.temperature(),
|
||||
}
|
||||
attributes = {}
|
||||
|
||||
attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active
|
||||
if self.simplisafe.temperature is not None:
|
||||
attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature
|
||||
|
||||
return attributes
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.17']
|
||||
REQUIREMENTS = ['total_connect_client==0.18']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the code format as regex."""
|
||||
return '^\\d{%s}$' % self._digits
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
|
||||
@@ -34,6 +34,8 @@ CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_RELAY_ADDR = 'relayaddr'
|
||||
CONF_RELAY_CHAN = 'relaychan'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
DEFAULT_DEVICE_HOST = 'localhost'
|
||||
@@ -53,6 +55,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
@@ -71,7 +74,11 @@ ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string})
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string,
|
||||
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte,
|
||||
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -153,6 +160,11 @@ def setup(hass, config):
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
def handle_rel_message(sender, message):
|
||||
"""Handle relay message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_REL_MESSAGE, message)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
@@ -171,6 +183,7 @@ def setup(hass, config):
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
controller.on_close += handle_closed_connection
|
||||
controller.on_relay_changed += handle_rel_message
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def turn_on(hass, entity_id):
|
||||
def async_turn_on(hass, entity_id):
|
||||
"""Async reset the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def turn_off(hass, entity_id):
|
||||
def async_turn_off(hass, entity_id):
|
||||
"""Async acknowledge the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def toggle(hass, entity_id):
|
||||
def async_toggle(hass, entity_id):
|
||||
"""Async toggle acknowledgement of alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class Alert(ToggleEntity):
|
||||
else:
|
||||
yield from self._schedule_notify()
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
@@ -228,7 +228,7 @@ class Alert(ToggleEntity):
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
yield from self._notify_done_message()
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _schedule_notify(self):
|
||||
|
||||
@@ -210,7 +210,7 @@ def resolve_slot_synonyms(key, request):
|
||||
return resolved_value
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
class AlexaResponse:
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent_info):
|
||||
|
||||
@@ -55,7 +55,7 @@ HANDLERS = Registry()
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
||||
|
||||
class _DisplayCategory(object):
|
||||
class _DisplayCategory:
|
||||
"""Possible display categories for Discovery response.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
||||
@@ -107,7 +107,6 @@ class _DisplayCategory(object):
|
||||
THERMOSTAT = "THERMOSTAT"
|
||||
|
||||
# Indicates the endpoint is a television.
|
||||
# pylint: disable=invalid-name
|
||||
TV = "TV"
|
||||
|
||||
|
||||
@@ -154,7 +153,7 @@ class _UnsupportedProperty(Exception):
|
||||
"""This entity does not support the requested Smart Home API property."""
|
||||
|
||||
|
||||
class _AlexaEntity(object):
|
||||
class _AlexaEntity:
|
||||
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||
|
||||
The API handlers should manipulate entities only through this interface.
|
||||
@@ -209,7 +208,7 @@ class _AlexaEntity(object):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _AlexaInterface(object):
|
||||
class _AlexaInterface:
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
|
||||
@@ -271,11 +270,14 @@ class _AlexaInterface(object):
|
||||
"""Return properties serialized for an API response."""
|
||||
for prop in self.properties_supported():
|
||||
prop_name = prop['name']
|
||||
yield {
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': self.get_property(prop_name),
|
||||
}
|
||||
# pylint: disable=assignment-from-no-return
|
||||
prop_value = self.get_property(prop_name)
|
||||
if prop_value is not None:
|
||||
yield {
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': prop_value,
|
||||
}
|
||||
|
||||
|
||||
class _AlexaPowerController(_AlexaInterface):
|
||||
@@ -313,7 +315,7 @@ class _AlexaLockController(_AlexaInterface):
|
||||
|
||||
if self.entity.state == STATE_LOCKED:
|
||||
return 'LOCKED'
|
||||
elif self.entity.state == STATE_UNLOCKED:
|
||||
if self.entity.state == STATE_UNLOCKED:
|
||||
return 'UNLOCKED'
|
||||
return 'JAMMED'
|
||||
|
||||
@@ -439,14 +441,17 @@ class _AlexaThermostatController(_AlexaInterface):
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = None
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
elif name == 'upperSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
if temp is None:
|
||||
else:
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if temp is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
@@ -610,7 +615,7 @@ class _SensorCapabilities(_AlexaEntity):
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
|
||||
|
||||
class _Cause(object):
|
||||
class _Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
||||
@@ -1474,9 +1479,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
# Work around a pylint false positive due to
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.2']
|
||||
REQUIREMENTS = ['amcrest==1.2.3']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -164,7 +164,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice(object):
|
||||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
|
||||
@@ -214,11 +214,11 @@ def async_setup(hass, config):
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
|
||||
if sensors:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
@@ -226,7 +226,7 @@ def async_setup(hass, config):
|
||||
}, config))
|
||||
|
||||
if switches:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
@@ -234,7 +234,7 @@ def async_setup(hass, config):
|
||||
}, config))
|
||||
|
||||
if motion:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
|
||||
@@ -58,7 +58,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class APCUPSdData(object):
|
||||
class APCUPSdData:
|
||||
"""Stores the data retrieved from APCUPSd.
|
||||
|
||||
For each entity to use, acts as the single point responsible for fetching
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Rest API for Home Assistant.
|
||||
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
https://developers.home-assistant.io/docs/en/external_api_rest.html
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
@@ -11,31 +11,34 @@ import logging
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
|
||||
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
|
||||
URL_API_TEMPLATE, __version__)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
import homeassistant.remote as rem
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BASE_URL = 'base_url'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
|
||||
ATTR_VERSION = 'version'
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_PAYLOAD = 'ping'
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
@@ -62,23 +65,22 @@ class APIStatusView(HomeAssistantView):
|
||||
"""View to handle Status requests."""
|
||||
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
name = 'api:status'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
return self.json_message("API running.")
|
||||
|
||||
|
||||
class APIEventStream(HomeAssistantView):
|
||||
"""View to handle EventStream requests."""
|
||||
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
name = 'api:stream'
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
@@ -95,7 +97,7 @@ class APIEventStream(HomeAssistantView):
|
||||
if restrict and event.event_type not in restrict:
|
||||
return
|
||||
|
||||
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
|
||||
_LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
|
||||
|
||||
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
data = stop_obj
|
||||
@@ -111,7 +113,7 @@ class APIEventStream(HomeAssistantView):
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
|
||||
|
||||
# Fire off one message so browsers fire open event right away
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
@@ -126,25 +128,25 @@ class APIEventStream(HomeAssistantView):
|
||||
break
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
await response.write(msg.encode("UTF-8"))
|
||||
_LOGGER.debug(
|
||||
"STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
||||
await response.write(msg.encode('UTF-8'))
|
||||
except asyncio.TimeoutError:
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s ABORT", id(stop_obj))
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
"""View to handle Config requests."""
|
||||
"""View to handle Configuration requests."""
|
||||
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
name = 'api:config'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -153,22 +155,22 @@ class APIConfigView(HomeAssistantView):
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
"""View to provide discovery info."""
|
||||
"""View to provide Discovery information."""
|
||||
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
name = 'api:discovery'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
"""Get discovery information."""
|
||||
hass = request.app['hass']
|
||||
needs_auth = hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': hass.config.api.base_url,
|
||||
'location_name': hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
ATTR_BASE_URL: hass.config.api.base_url,
|
||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
||||
ATTR_VERSION: __version__,
|
||||
})
|
||||
|
||||
|
||||
@@ -187,8 +189,8 @@ class APIStatesView(HomeAssistantView):
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
url = "/api/states/{entity_id}"
|
||||
name = "api:entity-state"
|
||||
url = '/api/states/{entity_id}'
|
||||
name = 'api:entity-state'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
@@ -196,7 +198,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
@@ -204,13 +206,13 @@ class APIEntityStateView(HomeAssistantView):
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if new_state is None:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
@@ -218,7 +220,8 @@ class APIEntityStateView(HomeAssistantView):
|
||||
is_new_state = hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update,
|
||||
self.context(request))
|
||||
|
||||
# Read the state back for our response
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
@@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView):
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity removed.")
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
"""View to handle EventListeners requests."""
|
||||
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
name = 'api:event-listeners'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/{event_type}'
|
||||
name = "api:event"
|
||||
name = 'api:event'
|
||||
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
@@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView):
|
||||
try:
|
||||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Event data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Event data should be a JSON object", HTTP_BAD_REQUEST)
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
@@ -276,8 +279,9 @@ class APIEventView(HomeAssistantView):
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
request.app['hass'].bus.async_fire(event_type, event_data,
|
||||
ha.EventOrigin.remote)
|
||||
request.app['hass'].bus.async_fire(
|
||||
event_type, event_data, ha.EventOrigin.remote,
|
||||
self.context(request))
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
@@ -286,7 +290,7 @@ class APIServicesView(HomeAssistantView):
|
||||
"""View to handle Services requests."""
|
||||
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
name = 'api:services'
|
||||
|
||||
async def get(self, request):
|
||||
"""Get registered services."""
|
||||
@@ -297,8 +301,8 @@ class APIServicesView(HomeAssistantView):
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
url = '/api/services/{domain}/{service}'
|
||||
name = 'api:domain-services'
|
||||
|
||||
async def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
@@ -310,11 +314,12 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
await hass.services.async_call(domain, service, data, True)
|
||||
await hass.services.async_call(
|
||||
domain, service, data, True, self.context(request))
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
@@ -323,7 +328,7 @@ class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
name = 'api:components'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -332,10 +337,10 @@ class APIComponentsView(HomeAssistantView):
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle requests."""
|
||||
"""View to handle Template requests."""
|
||||
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
name = 'api:template'
|
||||
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
@@ -344,29 +349,29 @@ class APITemplateView(HomeAssistantView):
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
return tpl.async_render(data.get('variables'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
class APIErrorLog(HomeAssistantView):
|
||||
"""View to fetch the error log."""
|
||||
"""View to fetch the API error log."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error_log"
|
||||
name = 'api:error_log'
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
|
||||
|
||||
|
||||
async def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = await async_get_all_descriptions(hass)
|
||||
return [{"domain": key, "services": value}
|
||||
return [{'domain': key, 'services': value}
|
||||
for key, value in descriptions.items()]
|
||||
|
||||
|
||||
def async_events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{"event": key, "listener_count": value}
|
||||
return [{'event': key, 'listener_count': value}
|
||||
for key, value in hass.bus.async_listeners().items()]
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.9']
|
||||
REQUIREMENTS = ['pyatv==0.3.10']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,7 +45,7 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar('T') # pylint: disable=invalid-name
|
||||
|
||||
|
||||
# This version of ensure_list interprets an empty dict as no value
|
||||
@@ -218,10 +218,10 @@ def _setup_atv(hass, atv_config):
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class ArduinoBoard(object):
|
||||
class ArduinoBoard:
|
||||
"""Representation of an Arduino board."""
|
||||
|
||||
def __init__(self, port):
|
||||
|
||||
@@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.2']
|
||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,10 +29,16 @@ DOMAIN = 'arlo'
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SIGNAL_UPDATE_ARLO = "arlo_update"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -38,6 +48,7 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
scan_interval = conf.get(CONF_SCAN_INTERVAL)
|
||||
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
@@ -45,7 +56,17 @@ def setup(hass, config):
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
|
||||
# assign refresh period to base station thread
|
||||
arlo_base_station = next((
|
||||
station for station in arlo.base_stations), None)
|
||||
|
||||
if arlo_base_station is None:
|
||||
return False
|
||||
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
@@ -55,4 +76,17 @@ def setup(hass, config):
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
||||
# register service
|
||||
hass.services.register(DOMAIN, 'update', hub_refresh)
|
||||
|
||||
# register scan interval for ArloHub
|
||||
track_time_interval(hass, hub_refresh, scan_interval)
|
||||
return True
|
||||
|
||||
@@ -48,7 +48,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class AsteriskData(object):
|
||||
class AsteriskData:
|
||||
"""Store Asterisk mailbox data."""
|
||||
|
||||
def __init__(self, hass, host, port, password):
|
||||
|
||||
@@ -123,9 +123,9 @@ def setup_august(hass, config, api, authenticator):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
elif state == AuthenticationState.BAD_PASSWORD:
|
||||
if state == AuthenticationState.BAD_PASSWORD:
|
||||
return False
|
||||
elif state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
"""Component to allow users to login and get tokens.
|
||||
|
||||
All requests will require passing in a valid client ID and secret via HTTP
|
||||
Basic Auth.
|
||||
|
||||
# GET /auth/providers
|
||||
|
||||
Return a list of auth providers. Example:
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Local",
|
||||
"id": null,
|
||||
"type": "local_provider",
|
||||
}
|
||||
]
|
||||
|
||||
# POST /auth/login_flow
|
||||
|
||||
Create a login flow. Will return the first step of the flow.
|
||||
|
||||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||
are identified by type and id.
|
||||
|
||||
{
|
||||
"handler": ["local_provider", null]
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
flow for details.
|
||||
|
||||
{
|
||||
"data_schema": [
|
||||
{"name": "username", "type": "string"},
|
||||
{"name": "password", "type": "string"}
|
||||
],
|
||||
"errors": {},
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"step_id": "init",
|
||||
"type": "form"
|
||||
}
|
||||
|
||||
# POST /auth/login_flow/{flow_id}
|
||||
|
||||
Progress the flow. Most flows will be 1 page, but could optionally add extra
|
||||
login challenges, like TFA. Once the flow has finished, the returned step will
|
||||
have type "create_entry" and "result" key will contain an authorization code.
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
# POST /auth/token
|
||||
|
||||
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
||||
@@ -104,21 +47,27 @@ a limited expiration.
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http.ban import log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
|
||||
from .client import verify_client
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
from . import indieauth
|
||||
from . import login_flow
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
WS_TYPE_CURRENT_USER = 'auth/current_user'
|
||||
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -126,122 +75,58 @@ async def async_setup(hass, config):
|
||||
"""Component to allow users to login."""
|
||||
store_credentials, retrieve_credentials = _create_cred_store()
|
||||
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||
hass.http.register_view(
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
|
||||
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_credentials)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
@verify_client
|
||||
async def get(self, request, client_id):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.async_auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
async def post(self, request, client_id):
|
||||
"""Create a new login flow."""
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
|
||||
class LoginFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/auth/login_flow/{flow_id}'
|
||||
name = 'api:auth:login_flow:resource'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr, store_credentials):
|
||||
"""Initialize the login flow resource view."""
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def get(self, request):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
async def post(self, request, client_id, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class GrantTokenView(HomeAssistantView):
|
||||
"""View to grant tokens."""
|
||||
|
||||
url = '/auth/token'
|
||||
name = 'api:auth:token'
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""Initialize the grant token view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@verify_client
|
||||
async def post(self, request, client_id):
|
||||
@log_invalid_auth
|
||||
async def post(self, request):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(
|
||||
hass, client_id, data)
|
||||
return await self._async_handle_auth_code(hass, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client_id, data)
|
||||
if grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(hass, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, client_id, data):
|
||||
async def _async_handle_auth_code(self, hass, data):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
|
||||
code = data.get('code')
|
||||
|
||||
if code is None:
|
||||
@@ -254,9 +139,17 @@ class GrantTokenView(HomeAssistantView):
|
||||
if credentials is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
|
||||
user = await hass.auth.async_get_or_create_user(credentials)
|
||||
|
||||
if not user.is_active:
|
||||
return self.json({
|
||||
'error': 'access_denied',
|
||||
'error_description': 'User is not active',
|
||||
}, status_code=403)
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||
client_id)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
@@ -269,8 +162,15 @@ class GrantTokenView(HomeAssistantView):
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
|
||||
async def _async_handle_refresh_token(self, hass, client_id, data):
|
||||
async def _async_handle_refresh_token(self, hass, data):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is not None and not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
|
||||
token = data.get('refresh_token')
|
||||
|
||||
if token is None:
|
||||
@@ -280,11 +180,16 @@ class GrantTokenView(HomeAssistantView):
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token(token)
|
||||
|
||||
if refresh_token is None or refresh_token.client_id != client_id:
|
||||
if refresh_token is None:
|
||||
return self.json({
|
||||
'error': 'invalid_grant',
|
||||
}, status_code=400)
|
||||
|
||||
if refresh_token.client_id != client_id:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
@@ -333,12 +238,46 @@ def _create_cred_store():
|
||||
def store_credentials(client_id, credentials):
|
||||
"""Store credentials and return a code to retrieve it."""
|
||||
code = uuid.uuid4().hex
|
||||
temp_credentials[(client_id, code)] = credentials
|
||||
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
|
||||
return code
|
||||
|
||||
@callback
|
||||
def retrieve_credentials(client_id, code):
|
||||
"""Retrieve credentials."""
|
||||
return temp_credentials.pop((client_id, code), None)
|
||||
key = (client_id, code)
|
||||
|
||||
if key not in temp_credentials:
|
||||
return None
|
||||
|
||||
created, credentials = temp_credentials.pop(key)
|
||||
|
||||
# OAuth 4.2.1
|
||||
# The authorization code MUST expire shortly after it is issued to
|
||||
# mitigate the risk of leaks. A maximum authorization code lifetime of
|
||||
# 10 minutes is RECOMMENDED.
|
||||
if dt_util.utcnow() - created < timedelta(minutes=10):
|
||||
return credentials
|
||||
|
||||
return None
|
||||
|
||||
return store_credentials, retrieve_credentials
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_current_user(hass, connection, msg):
|
||||
"""Return the current user."""
|
||||
user = connection.request.get('hass_user')
|
||||
|
||||
if user is None:
|
||||
connection.to_write.put_nowait(websocket_api.error_message(
|
||||
msg['id'], 'no_user', 'Not authenticated as a user'))
|
||||
return
|
||||
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials]
|
||||
}))
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
import base64
|
||||
from functools import wraps
|
||||
import hmac
|
||||
|
||||
import aiohttp.hdrs
|
||||
|
||||
|
||||
def verify_client(method):
|
||||
"""Decorator to verify client id/secret on requests."""
|
||||
@wraps(method)
|
||||
async def wrapper(view, request, *args, **kwargs):
|
||||
"""Verify client id/secret before doing request."""
|
||||
client_id = await _verify_client(request)
|
||||
|
||||
if client_id is None:
|
||||
return view.json({
|
||||
'error': 'invalid_client',
|
||||
}, status_code=401)
|
||||
|
||||
return await method(
|
||||
view, request, *args, client_id=client_id, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def _verify_client(request):
|
||||
"""Method to verify the client id/secret in consistent time.
|
||||
|
||||
By using a consistent time for looking up client id and comparing the
|
||||
secret, we prevent attacks by malicious actors trying different client ids
|
||||
and are able to derive from the time it takes to process the request if
|
||||
they guessed the client id correctly.
|
||||
"""
|
||||
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
|
||||
return None
|
||||
|
||||
auth_type, auth_value = \
|
||||
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
|
||||
|
||||
if auth_type != 'Basic':
|
||||
return None
|
||||
|
||||
decoded = base64.b64decode(auth_value).decode('utf-8')
|
||||
try:
|
||||
client_id, client_secret = decoded.split(':', 1)
|
||||
except ValueError:
|
||||
# If no ':' in decoded
|
||||
return None
|
||||
|
||||
client = await request.app['hass'].auth.async_get_client(client_id)
|
||||
|
||||
if client is None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
if hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client_id
|
||||
|
||||
return None
|
||||
130
homeassistant/components/auth/indieauth.py
Normal file
130
homeassistant/components/auth/indieauth.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
from ipaddress import ip_address, ip_network
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
ALLOWED_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
)
|
||||
|
||||
|
||||
def verify_redirect_uri(client_id, redirect_uri):
|
||||
"""Verify that the client and redirect uri match."""
|
||||
try:
|
||||
client_id_parts = _parse_client_id(client_id)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
redirect_parts = _parse_url(redirect_uri)
|
||||
|
||||
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
|
||||
# but needs to be specified in link tag when fetching `client_id`.
|
||||
# This is not implemented.
|
||||
|
||||
# Verify redirect url and client url have same scheme and domain.
|
||||
return (
|
||||
client_id_parts.scheme == redirect_parts.scheme and
|
||||
client_id_parts.netloc == redirect_parts.netloc
|
||||
)
|
||||
|
||||
|
||||
def verify_client_id(client_id):
|
||||
"""Verify that the client id is valid."""
|
||||
try:
|
||||
_parse_client_id(client_id)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _parse_url(url):
|
||||
"""Parse a url in parts and canonicalize according to IndieAuth."""
|
||||
parts = urlparse(url)
|
||||
|
||||
# Canonicalize a url according to IndieAuth 3.2.
|
||||
|
||||
# SHOULD convert the hostname to lowercase
|
||||
parts = parts._replace(netloc=parts.netloc.lower())
|
||||
|
||||
# If a URL with no path component is ever encountered,
|
||||
# it MUST be treated as if it had the path /.
|
||||
if parts.path == '':
|
||||
parts = parts._replace(path='/')
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
def _parse_client_id(client_id):
|
||||
"""Test if client id is a valid URL according to IndieAuth section 3.2.
|
||||
|
||||
https://indieauth.spec.indieweb.org/#client-identifier
|
||||
"""
|
||||
parts = _parse_url(client_id)
|
||||
|
||||
# Client identifier URLs
|
||||
# MUST have either an https or http scheme
|
||||
if parts.scheme not in ('http', 'https'):
|
||||
raise ValueError()
|
||||
|
||||
# MUST contain a path component
|
||||
# Handled by url canonicalization.
|
||||
|
||||
# MUST NOT contain single-dot or double-dot path segments
|
||||
if any(segment in ('.', '..') for segment in parts.path.split('/')):
|
||||
raise ValueError(
|
||||
'Client ID cannot contain single-dot or double-dot path segments')
|
||||
|
||||
# MUST NOT contain a fragment component
|
||||
if parts.fragment != '':
|
||||
raise ValueError('Client ID cannot contain a fragment')
|
||||
|
||||
# MUST NOT contain a username or password component
|
||||
if parts.username is not None:
|
||||
raise ValueError('Client ID cannot contain username')
|
||||
|
||||
if parts.password is not None:
|
||||
raise ValueError('Client ID cannot contain password')
|
||||
|
||||
# MAY contain a port
|
||||
try:
|
||||
# parts raises ValueError when port cannot be parsed as int
|
||||
parts.port
|
||||
except ValueError:
|
||||
raise ValueError('Client ID contains invalid port')
|
||||
|
||||
# Additionally, hostnames
|
||||
# MUST be domain names or a loopback interface and
|
||||
# MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1
|
||||
# or IPv6 [::1]
|
||||
|
||||
# We are not goint to follow the spec here. We are going to allow
|
||||
# any internal network IP to be used inside a client id.
|
||||
|
||||
address = None
|
||||
|
||||
try:
|
||||
netloc = parts.netloc
|
||||
|
||||
# Strip the [, ] from ipv6 addresses before parsing
|
||||
if netloc[0] == '[' and netloc[-1] == ']':
|
||||
netloc = netloc[1:-1]
|
||||
|
||||
address = ip_address(netloc)
|
||||
except ValueError:
|
||||
# Not an ip address
|
||||
pass
|
||||
|
||||
if (address is None or
|
||||
address in ALLOWED_IPS or
|
||||
any(address in network for network in ALLOWED_NETWORKS)):
|
||||
return parts
|
||||
|
||||
raise ValueError('Hostname should be a domain name or local IP address')
|
||||
172
homeassistant/components/auth/login_flow.py
Normal file
172
homeassistant/components/auth/login_flow.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""HTTP views handle login flow.
|
||||
|
||||
# GET /auth/providers
|
||||
|
||||
Return a list of auth providers. Example:
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Local",
|
||||
"id": null,
|
||||
"type": "local_provider",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# POST /auth/login_flow
|
||||
|
||||
Create a login flow. Will return the first step of the flow.
|
||||
|
||||
Pass in parameter 'client_id' and 'redirect_url' validate by indieauth.
|
||||
|
||||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||
are identified by type and id.
|
||||
|
||||
{
|
||||
"client_id": "https://hassbian.local:8123/",
|
||||
"handler": ["local_provider", null],
|
||||
"redirect_url": "https://hassbian.local:8123/"
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
flow for details.
|
||||
|
||||
{
|
||||
"data_schema": [
|
||||
{"name": "username", "type": "string"},
|
||||
{"name": "password", "type": "string"}
|
||||
],
|
||||
"errors": {},
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"step_id": "init",
|
||||
"type": "form"
|
||||
}
|
||||
|
||||
|
||||
# POST /auth/login_flow/{flow_id}
|
||||
|
||||
Progress the flow. Most flows will be 1 page, but could optionally add extra
|
||||
login challenges, like TFA. Once the flow has finished, the returned step will
|
||||
have type "create_entry" and "result" key will contain an authorization code.
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
}
|
||||
"""
|
||||
import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.http.ban import process_wrong_login, \
|
||||
log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from . import indieauth
|
||||
|
||||
|
||||
async def async_setup(hass, store_credentials):
|
||||
"""Component to allow users to login."""
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||
hass.http.register_view(
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
if not indieauth.verify_redirect_uri(data['client_id'],
|
||||
data['redirect_uri']):
|
||||
return self.json_message('invalid client id or redirect uri', 400)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
|
||||
class LoginFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/auth/login_flow/{flow_id}'
|
||||
name = 'api:auth:login_flow:resource'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr, store_credentials):
|
||||
"""Initialize the login flow resource view."""
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
async def get(self, request, flow_id):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'client_id': str
|
||||
}, extra=vol.ALLOW_EXTRA))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
client_id = data.pop('client_id')
|
||||
|
||||
if not indieauth.verify_client_id(client_id):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||
# need manually log failed login attempts
|
||||
if result['errors'] is not None and \
|
||||
result['errors'].get('base') == 'invalid_auth':
|
||||
await process_wrong_login(request)
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
@@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
||||
})
|
||||
|
||||
@@ -297,7 +297,7 @@ class AutomationEntity(ToggleEntity):
|
||||
return
|
||||
|
||||
# HomeAssistant is starting up
|
||||
elif self.hass.state == CoreState.not_running:
|
||||
if self.hass.state == CoreState.not_running:
|
||||
@asyncio.coroutine
|
||||
def async_enable_automation(event):
|
||||
"""Start automation on startup."""
|
||||
|
||||
@@ -44,7 +44,7 @@ def async_trigger(hass, config, action):
|
||||
|
||||
# Automation are enabled while hass is starting up, fire right away
|
||||
# Check state because a config reload shouldn't trigger it.
|
||||
elif hass.state == CoreState.starting:
|
||||
if hass.state == CoreState.starting:
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
def _shutdown(call):
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||
@@ -272,8 +272,7 @@ class AxisDeviceEvent(Entity):
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.update()
|
||||
self.schedule_update_ha_state()
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -16,11 +16,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'bbb_gpio'
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, config):
|
||||
"""Set up the BeagleBone Black GPIO component."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
|
||||
def cleanup_gpio(event):
|
||||
"""Stuff to do before stopping."""
|
||||
@@ -34,41 +33,39 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
# noqa: F821
|
||||
|
||||
def setup_output(pin):
|
||||
"""Set up a GPIO as output."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(pin, pull_mode):
|
||||
"""Set up a GPIO as input."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.IN, # noqa: F821
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821
|
||||
else GPIO.PUD_UP) # noqa: F821
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.setup(pin, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||
else GPIO.PUD_UP)
|
||||
|
||||
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
||||
@@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry):
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE)
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
@@ -37,8 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
||||
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
||||
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
zone_num, zone_name, zone_type, zone_rfid)
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
||||
devices.append(device)
|
||||
|
||||
add_devices(devices)
|
||||
@@ -49,7 +52,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
|
||||
relay_addr, relay_chan):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
@@ -57,6 +61,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self._name = zone_name
|
||||
self._rfid = zone_rfid
|
||||
self._rfstate = None
|
||||
self._relay_addr = relay_addr
|
||||
self._relay_chan = relay_chan
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -70,6 +76,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_REL_MESSAGE, self._rel_message_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
@@ -122,3 +131,12 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
self._rfstate = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _rel_message_callback(self, message):
|
||||
"""Update relay state."""
|
||||
if (self._relay_addr == message.address and
|
||||
self._relay_chan == message.channel):
|
||||
_LOGGER.debug("Relay %d:%d value:%d", message.address,
|
||||
message.channel, message.value)
|
||||
self._state = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -89,7 +89,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self.arest.update()
|
||||
|
||||
|
||||
class ArestData(object):
|
||||
class ArestData:
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, resource, pin):
|
||||
|
||||
@@ -99,7 +99,7 @@ class AuroraSensor(BinarySensorDevice):
|
||||
self.aurora_data.update()
|
||||
|
||||
|
||||
class AuroraData(object):
|
||||
class AuroraData:
|
||||
"""Get aurora forecast."""
|
||||
|
||||
def __init__(self, latitude, longitude, threshold):
|
||||
|
||||
@@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
||||
self._deviation = bool(self.probability >= self._probability_threshold)
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.bbb_gpio as bbb_gpio
|
||||
from homeassistant.components import bbb_gpio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
||||
|
||||
@@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety']
|
||||
'door_lock_state': ['Door lock state', 'safety'],
|
||||
'lights_parking': ['Parking lights', 'light'],
|
||||
'condition_based_services': ['Condition based services', 'problem'],
|
||||
'check_control_messages': ['Control messages', 'problem']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC = {
|
||||
'charging_status': ['Charging status', 'power'],
|
||||
'connection_status': ['Connection status', 'plug']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
@@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
if vehicle.has_hv_battery:
|
||||
_LOGGER.debug('BMW with a high voltage battery')
|
||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
elif vehicle.has_internal_combustion_engine:
|
||||
_LOGGER.debug('BMW with an internal combustion engine')
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
@@ -92,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
result['last_update_reason'] = vehicle_state.last_update_reason
|
||||
elif self._attribute == 'lights_parking':
|
||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == 'condition_based_services':
|
||||
for report in vehicle_state.condition_based_services:
|
||||
result.update(self._format_cbs_report(report))
|
||||
elif self._attribute == 'check_control_messages':
|
||||
check_control_messages = vehicle_state.check_control_messages
|
||||
if not check_control_messages:
|
||||
result['check_control_messages'] = 'OK'
|
||||
else:
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
# pylint: disable=protected-access
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=protected-access
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
|
||||
return result
|
||||
return sorted(result.items())
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
from bimmer_connected.state import ChargingState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
@@ -111,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == 'lights_parking':
|
||||
self._state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == 'condition_based_services':
|
||||
self._state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == 'check_control_messages':
|
||||
self._state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == 'charging_status':
|
||||
self._state = vehicle_state.charging_status in \
|
||||
[ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=protected-access
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
@staticmethod
|
||||
def _format_cbs_report(report):
|
||||
result = {}
|
||||
service_type = report.service_type.lower().replace('_', ' ')
|
||||
result['{} status'.format(service_type)] = report.state.value
|
||||
if report.due_date is not None:
|
||||
result['{} date'.format(service_type)] = \
|
||||
report.due_date.strftime('%Y-%m-%d')
|
||||
if report.due_distance is not None:
|
||||
result['{} distance'.format(service_type)] = \
|
||||
'{} km'.format(report.due_distance)
|
||||
return result
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
|
||||
@@ -25,6 +25,9 @@ DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
CONF_COMMAND_TIMEOUT = 'command_timeout'
|
||||
DEFAULT_TIMEOUT = 15
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -32,10 +35,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Command line Binary Sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -44,9 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
command_timeout = config.get(CONF_COMMAND_TIMEOUT)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
|
||||
@@ -5,8 +5,9 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
entities = []
|
||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
|
||||
@@ -58,7 +62,8 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr']:
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -103,6 +108,8 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark:
|
||||
attr['dark'] = self._sensor.dark
|
||||
if self._sensor.on is not None:
|
||||
attr[ATTR_ON] = self._sensor.on
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
|
||||
attr[ATTR_DARK] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,6 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
ATTR_DROPLET_ID: self.data.id,
|
||||
ATTR_DROPLET_NAME: self.data.name,
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.eight_sleep/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.eight_sleep import (
|
||||
@@ -16,8 +15,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['eight_sleep']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the eight sleep binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -63,7 +62,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = self._usrobj.bed_presence
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -14,6 +15,7 @@ from homeassistant.components.envisalink import (
|
||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
|
||||
# The Envisalink library returns a "last_fault" value that's the
|
||||
# number of seconds since the last fault, up to a maximum of 327680
|
||||
# seconds (65536 5-second ticks).
|
||||
#
|
||||
# We don't want the HA event log to fill up with a bunch of no-op
|
||||
# "state changes" that are just that number ticking up once per poll
|
||||
# interval, so we subtract it from the current second-accurate time
|
||||
# unless it is already at the maximum value, in which case we set it
|
||||
# to None since we can't determine the actual value.
|
||||
seconds_ago = self._info['last_fault']
|
||||
if seconds_ago < 65536 * 5:
|
||||
now = dt_util.now().replace(microsecond=0)
|
||||
delta = datetime.timedelta(seconds=seconds_ago)
|
||||
last_trip_time = (now - delta).isoformat()
|
||||
else:
|
||||
last_trip_time = None
|
||||
|
||||
attr[ATTR_LAST_TRIP_TIME] = last_trip_time
|
||||
return attr
|
||||
|
||||
@property
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||
REQUIREMENTS = ['pyflic-homeassistant==0.4.dev0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the GC100 devices."""
|
||||
binary_sensors = []
|
||||
@@ -40,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, port_addr, gc100):
|
||||
"""Initialize the GC100 binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port_addr = port_addr
|
||||
self._gc100 = gc100
|
||||
|
||||
@@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
class HikvisionData:
|
||||
"""Hikvision device event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
|
||||
88
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
88
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Support for HomematicIP binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_WINDOW_STATE = 'window_state'
|
||||
ATTR_EVENT_DELAY = 'event_delay'
|
||||
ATTR_MOTION_DETECTED = 'motion_detected'
|
||||
ATTR_ILLUMINATION = 'illumination'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the binary sensor devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP binary sensor from a config entry."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
devices.append(HomematicipShutterContact(home, device))
|
||||
elif isinstance(device, MotionDetectorIndoor):
|
||||
devices.append(HomematicipMotionDetector(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP shutter contact."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'door'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the shutter contact is on/open."""
|
||||
from homematicip.base.enums import WindowState
|
||||
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
if self._device.windowState is None:
|
||||
return None
|
||||
return self._device.windowState == WindowState.OPEN
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""MomematicIP motion detector."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'motion'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if motion is detected."""
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
return self._device.motionDetected
|
||||
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Hydrawise sprinkler.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hydrawise/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.hydrawise import (
|
||||
BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP,
|
||||
DEVICE_MAP_INDEX)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
|
||||
DEPENDENCIES = ['hydrawise']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
hydrawise = hass.data[DATA_HYDRAWISE].data
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
if sensor_type in ['status', 'rain_sensor']:
|
||||
sensors.append(
|
||||
HydrawiseBinarySensor(
|
||||
hydrawise.controller_status, sensor_type))
|
||||
|
||||
else:
|
||||
# create a sensor for each zone
|
||||
for zone in hydrawise.relays:
|
||||
zone_data = zone
|
||||
zone_data['running'] = \
|
||||
hydrawise.controller_status.get('running', False)
|
||||
sensors.append(HydrawiseBinarySensor(zone_data, sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name)
|
||||
mydata = self.hass.data[DATA_HYDRAWISE].data
|
||||
if self._sensor_type == 'status':
|
||||
self._state = mydata.status == 'All good!'
|
||||
elif self._sensor_type == 'rain_sensor':
|
||||
for sensor in mydata.sensors:
|
||||
if sensor['name'] == 'Rain':
|
||||
self._state = sensor['active'] == 1
|
||||
elif self._sensor_type == 'is_watering':
|
||||
if not mydata.running:
|
||||
self._state = False
|
||||
elif int(mydata.running[0]['relay']) == self.data['relay']:
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor type."""
|
||||
return DEVICE_MAP[self._sensor_type][
|
||||
DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')]
|
||||
@@ -3,8 +3,6 @@
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ihc/
|
||||
"""
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -70,7 +68,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
||||
|
||||
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||
sensor_type: str, inverting: bool,
|
||||
product: Element = None) -> None:
|
||||
product=None) -> None:
|
||||
"""Initialize the IHC binary sensor."""
|
||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||
self._state = None
|
||||
|
||||
@@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'wetLeakSensor': 'moisture'}
|
||||
'wetLeakSensor': 'moisture',
|
||||
'lightSensor': 'light',
|
||||
'batterySensor': 'battery'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -54,4 +56,9 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
return bool(self._insteon_device_state.value)
|
||||
on_val = bool(self._insteon_device_state.value)
|
||||
|
||||
if self._insteon_device_state.name == 'lightSensor':
|
||||
return not on_val
|
||||
|
||||
return on_val
|
||||
|
||||
@@ -101,7 +101,7 @@ class IssBinarySensor(BinarySensorDevice):
|
||||
self.iss_data.update()
|
||||
|
||||
|
||||
class IssData(object):
|
||||
class IssData:
|
||||
"""Get data from the ISS API."""
|
||||
|
||||
def __init__(self, latitude, longitude):
|
||||
|
||||
@@ -8,7 +8,7 @@ https://home-assistant.io/components/binary_sensor.isy994/
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
@@ -28,7 +28,6 @@ ISY_DEVICE_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
@@ -56,7 +55,7 @@ def setup_platform(hass, config: ConfigType,
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if (device_type == 'opening' or device_type == 'moisture'):
|
||||
if device_type in ('opening', 'moisture'):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
if subnode_id == 2:
|
||||
@@ -117,8 +116,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
self._status_was_unknown = True
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
self._status_was_unknown = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
@@ -156,9 +157,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
# in use for this device. Next we need to check to see if the
|
||||
# negative and positive nodes disagree on the state (both ON or
|
||||
# both OFF).
|
||||
if self._negative_node.status._val == self._node.status._val:
|
||||
# The states disagree, therefore we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
@@ -189,14 +194,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
"""Primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
We MOSTLY ignore these updates, as we listen directly to the Control
|
||||
events on all nodes for this device. However, there is one edge case:
|
||||
If a leak sensor is unknown, due to a recent reboot of the ISY, the
|
||||
status will get updated to dry upon the first heartbeat. This status
|
||||
update is the only way that a leak sensor's status changes without
|
||||
an accompanying Control event, so we need to watch for it.
|
||||
"""
|
||||
pass
|
||||
if self._status_was_unknown and self._computed_state is None:
|
||||
self._computed_state = bool(int(self._node.status))
|
||||
self._status_was_unknown = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
@@ -286,7 +298,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
# No heartbeat timer is active
|
||||
pass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def timer_elapsed(now) -> None:
|
||||
"""Heartbeat missed; set state to indicate dead battery."""
|
||||
@@ -301,7 +312,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
self._heartbeat_timer = async_track_point_in_utc_time(
|
||||
self.hass, timer_elapsed, point_in_time)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
|
||||
82
homeassistant/components/binary_sensor/konnected.py
Normal file
82
homeassistant/components/binary_sensor/konnected.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for wired binary sensors attached to a Konnected device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.konnected/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.konnected import (
|
||||
DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID,
|
||||
ATTR_STATE)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['konnected']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensors attached to a Konnected device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[KONNECTED_DOMAIN]
|
||||
device_id = discovery_info['device_id']
|
||||
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
|
||||
for pin_num, pin_data in
|
||||
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
|
||||
async_add_devices(sensors)
|
||||
|
||||
|
||||
class KonnectedBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Konnected binary sensor."""
|
||||
|
||||
def __init__(self, device_id, pin_num, data):
|
||||
"""Initialize the binary sensor."""
|
||||
self._data = data
|
||||
self._device_id = device_id
|
||||
self._pin_num = pin_num
|
||||
self._state = self._data.get(ATTR_STATE)
|
||||
self._device_class = self._data.get(CONF_TYPE)
|
||||
self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format(
|
||||
device_id, PIN_TO_ZONE[pin_num]))
|
||||
_LOGGER.debug('Created new Konnected sensor: %s', self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store entity_id and register state change callback."""
|
||||
self._data[ATTR_ENTITY_ID] = self.entity_id
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id),
|
||||
self.async_set_state)
|
||||
|
||||
@callback
|
||||
def async_set_state(self, state):
|
||||
"""Update the sensor's state."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -52,19 +52,18 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
self._node_id = node_id
|
||||
self._state = None
|
||||
self.data = None
|
||||
self._attrs = {}
|
||||
self._name = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
if self.data is not None:
|
||||
return self.data.label
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.data is not None:
|
||||
return self.data.status == 'running'
|
||||
return False
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -74,8 +73,18 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Linode Node."""
|
||||
if self.data:
|
||||
return {
|
||||
return self._attrs
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._linode.update()
|
||||
if self._linode.data is not None:
|
||||
for node in self._linode.data:
|
||||
if node.id == self._node_id:
|
||||
self.data = node
|
||||
if self.data is not None:
|
||||
self._state = self.data.status == 'running'
|
||||
self._attrs = {
|
||||
ATTR_CREATED: self.data.created,
|
||||
ATTR_NODE_ID: self.data.id,
|
||||
ATTR_NODE_NAME: self.data.label,
|
||||
@@ -85,12 +94,4 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
ATTR_REGION: self.data.region.country,
|
||||
ATTR_VCPUS: self.data.specs.vcpus,
|
||||
}
|
||||
return {}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._linode.update()
|
||||
if self._linode.data is not None:
|
||||
for node in self._linode.data:
|
||||
if node.id == self._node_id:
|
||||
self.data = node
|
||||
self._name = self.data.label
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.modbus/
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.components import modbus
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -6,11 +6,12 @@ https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
@@ -24,7 +25,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
|
||||
CONF_UNIQUE_ID = 'unique_id'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
@@ -37,6 +38,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
# Integrations shouldn't never expose unique_id through configuration
|
||||
# this here is an exception because MQTT is a msg transport, not a protocol
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -61,7 +65,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
value_template
|
||||
value_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
)])
|
||||
|
||||
|
||||
@@ -70,7 +75,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, force_update, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template):
|
||||
payload_not_available, value_template,
|
||||
unique_id: Optional[str]):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@@ -83,6 +89,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
self._qos = qos
|
||||
self._force_update = force_update
|
||||
self._template = value_template
|
||||
self._unique_id = unique_id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -134,3 +141,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
hub = hass.data[MYCHEVY_DOMAIN]
|
||||
for sconfig in SENSORS:
|
||||
sensors.append(EVBinarySensor(hub, sconfig))
|
||||
for car in hub.cars:
|
||||
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
|
||||
|
||||
async_add_devices(sensors)
|
||||
|
||||
@@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, connection, config):
|
||||
def __init__(self, connection, config, car_vid):
|
||||
"""Initialize sensor with car connection."""
|
||||
self._conn = connection
|
||||
self._name = config.name
|
||||
self._attr = config.attr
|
||||
self._type = config.device_class
|
||||
self._is_on = None
|
||||
|
||||
self._car_vid = car_vid
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name)))
|
||||
'{}_{}_{}'.format(MYCHEVY_DOMAIN,
|
||||
slugify(self._car.name),
|
||||
slugify(self._name)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
"""Return if on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def _car(self):
|
||||
"""Return the car."""
|
||||
return self._conn.get_car(self._car_vid)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self._conn.car is not None:
|
||||
self._is_on = getattr(self._conn.car, self._attr, None)
|
||||
if self._car is not None:
|
||||
self._is_on = getattr(self._car, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
||||
@@ -29,7 +29,8 @@ async def async_setup_platform(
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
class MySensorsBinarySensor(
|
||||
mysensors.device.MySensorsEntity, BinarySensorDevice):
|
||||
"""Representation of a MySensors Binary Sensor child node."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -29,6 +29,7 @@ class MyStromView(HomeAssistantView):
|
||||
|
||||
url = '/api/mystrom'
|
||||
name = 'api:mystrom'
|
||||
supported_actions = ['single', 'double', 'long', 'touch']
|
||||
|
||||
def __init__(self, add_devices):
|
||||
"""Initialize the myStrom URL endpoint."""
|
||||
@@ -44,16 +45,18 @@ class MyStromView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def _handle(self, hass, data):
|
||||
"""Handle requests to the myStrom endpoint."""
|
||||
button_action = list(data.keys())[0]
|
||||
button_id = data[button_action]
|
||||
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
|
||||
button_action = next((
|
||||
parameter for parameter in data
|
||||
if parameter in self.supported_actions), None)
|
||||
|
||||
if button_action not in ['single', 'double', 'long', 'touch']:
|
||||
if button_action is None:
|
||||
_LOGGER.error(
|
||||
"Received unidentified message from myStrom button: %s", data)
|
||||
return ("Received unidentified message: {}".format(data),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
button_id = data[button_action]
|
||||
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
|
||||
if entity_id not in self.buttons:
|
||||
_LOGGER.info("New myStrom button/action detected: %s/%s",
|
||||
button_id, button_action)
|
||||
|
||||
@@ -7,27 +7,35 @@ https://home-assistant.io/components/binary_sensor.nest/
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.nest import (
|
||||
DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
|
||||
BINARY_TYPES = ['online']
|
||||
BINARY_TYPES = {'online': 'connectivity'}
|
||||
|
||||
CLIMATE_BINARY_TYPES = [
|
||||
'fan',
|
||||
'is_using_emergency_heat',
|
||||
'is_locked',
|
||||
'has_leaf',
|
||||
]
|
||||
CLIMATE_BINARY_TYPES = {
|
||||
'fan': None,
|
||||
'is_using_emergency_heat': 'heat',
|
||||
'is_locked': None,
|
||||
'has_leaf': None,
|
||||
}
|
||||
|
||||
CAMERA_BINARY_TYPES = [
|
||||
'motion_detected',
|
||||
'sound_detected',
|
||||
'person_detected',
|
||||
]
|
||||
CAMERA_BINARY_TYPES = {
|
||||
'motion_detected': 'motion',
|
||||
'sound_detected': 'sound',
|
||||
'person_detected': 'occupancy',
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_TYPES = {
|
||||
'away': None,
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_STATE_MAP = {
|
||||
'away': {'away': True, 'home': False},
|
||||
}
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_ac_state',
|
||||
@@ -40,19 +48,26 @@ _BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_emer_heat_state',
|
||||
]
|
||||
|
||||
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
|
||||
+ CAMERA_BINARY_TYPES
|
||||
_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES,
|
||||
**CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up the Nest binary sensors.
|
||||
|
||||
No longer used.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Set up a Nest binary sensor based on a config entry."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
|
||||
discovery_info = \
|
||||
hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
|
||||
|
||||
# Add all available binary sensors if no Nest binary sensor config is set
|
||||
if discovery_info == {}:
|
||||
conditions = _VALID_BINARY_SENSOR_TYPES
|
||||
@@ -67,32 +82,40 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"for valid options.")
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and device.is_thermostat]
|
||||
|
||||
if device.is_camera:
|
||||
def get_binary_sensors():
|
||||
"""Get the Nest binary sensors."""
|
||||
sensors = []
|
||||
for structure in nest.structures():
|
||||
sensors += [NestBinarySensor(structure, None, variable)
|
||||
for variable in conditions
|
||||
if variable in STRUCTURE_BINARY_TYPES]
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and device.is_thermostat]
|
||||
|
||||
add_devices(sensors, True)
|
||||
if device.is_camera:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conditions
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
|
||||
return sensors
|
||||
|
||||
async_add_devices(await hass.async_add_job(get_binary_sensors), True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
"""Represents a Nest binary sensor."""
|
||||
|
||||
@property
|
||||
@@ -100,9 +123,19 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the binary sensor."""
|
||||
return _VALID_BINARY_SENSOR_TYPES.get(self.variable)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = bool(getattr(self.device, self.variable))
|
||||
value = getattr(self.device, self.variable)
|
||||
if self.variable in STRUCTURE_BINARY_TYPES:
|
||||
self._state = bool(STRUCTURE_BINARY_STATE_MAP
|
||||
[self.variable].get(value))
|
||||
else:
|
||||
self._state = bool(value)
|
||||
|
||||
|
||||
class NestActivityZoneSensor(NestBinarySensor):
|
||||
@@ -115,9 +148,9 @@ class NestActivityZoneSensor(NestBinarySensor):
|
||||
self._name = "{} {} activity".format(self._name, self.zone.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
def device_class(self):
|
||||
"""Return the device class of the binary sensor."""
|
||||
return 'motion'
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
|
||||
@@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the access to Netatmo binary sensor."""
|
||||
netatmo = hass.components.netatmo
|
||||
@@ -68,12 +67,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
module_name = None
|
||||
|
||||
import lnetatmo
|
||||
import pyatmo
|
||||
try:
|
||||
data = CameraData(netatmo.NETATMO_AUTH, home)
|
||||
if not data.get_camera_names():
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
except pyatmo.NoDevice:
|
||||
return None
|
||||
|
||||
welcome_sensors = config.get(
|
||||
@@ -143,7 +142,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
if self._cameratype == 'NACamera':
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
if self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
|
||||
@@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Pilight Binary Sensor."""
|
||||
disarm = config.get(CONF_DISARM_AFTER_TRIGGER)
|
||||
|
||||
@@ -96,7 +96,7 @@ class PingBinarySensor(BinarySensorDevice):
|
||||
self.ping.update()
|
||||
|
||||
|
||||
class PingData(object):
|
||||
class PingData:
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, host, count):
|
||||
|
||||
126
homeassistant/components/binary_sensor/rachio.py
Normal file
126
homeassistant/components/binary_sensor/rachio.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Integration with the Rachio Iro sprinkler system controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rachio/
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO,
|
||||
KEY_DEVICE_ID,
|
||||
KEY_STATUS,
|
||||
KEY_SUBTYPE,
|
||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
STATUS_OFFLINE,
|
||||
STATUS_ONLINE,
|
||||
SUBTYPE_OFFLINE,
|
||||
SUBTYPE_ONLINE,)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rachio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Rachio binary sensors."""
|
||||
devices = []
|
||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
||||
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
|
||||
|
||||
add_devices(devices)
|
||||
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
|
||||
|
||||
|
||||
class RachioControllerBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that reflects a Rachio state."""
|
||||
|
||||
def __init__(self, hass, controller, poll=True):
|
||||
"""Set up a new Rachio controller binary sensor."""
|
||||
self._controller = controller
|
||||
|
||||
if poll:
|
||||
self._state = self._poll_update()
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
self._handle_any_update)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Declare that this entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the sensor has a 'true' value."""
|
||||
return self._state
|
||||
|
||||
def _handle_any_update(self, *args, **kwargs) -> None:
|
||||
"""Determine whether an update event applies to this device."""
|
||||
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
|
||||
# For another device
|
||||
return
|
||||
|
||||
# For this device
|
||||
self._handle_update()
|
||||
|
||||
@abstractmethod
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
pass
|
||||
|
||||
|
||||
class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
|
||||
"""Represent a binary sensor that reflects if the controller is online."""
|
||||
|
||||
def __init__(self, hass, controller):
|
||||
"""Set up a new Rachio controller online binary sensor."""
|
||||
super().__init__(hass, controller, poll=False)
|
||||
self._state = self._poll_update(controller.init_data)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of this sensor including the controller name."""
|
||||
return "{} online".format(self._controller.name)
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'connectivity'
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the name of an icon for this sensor."""
|
||||
return 'mdi:wifi-strength-4' if self.is_on\
|
||||
else 'mdi:wifi-strength-off-outline'
|
||||
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
if data is None:
|
||||
data = self._controller.rachio.device.get(
|
||||
self._controller.controller_id)[1]
|
||||
|
||||
if data[KEY_STATUS] == STATUS_ONLINE:
|
||||
return True
|
||||
if data[KEY_STATUS] == STATUS_OFFLINE:
|
||||
return False
|
||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||
data[KEY_STATUS])
|
||||
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE:
|
||||
self._state = True
|
||||
elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
|
||||
self._state = False
|
||||
|
||||
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